Using External VRPN Devices in OSVR

Ryan A. Pavlik1, April 27, 2015

Introduction and Requirements

In order to provide both proven performance and wide compatibility, portions of OSVR use and extend the VRPN (Virtual Reality Peripheral Network) software and device model. Early releases of OSVR provided an experimental way to integrate external VRPN trackers into OSVR, but the 0.2 release removed that experimental support in favor of complete support for using tracker, button, and analog devices from an external (local or remote) VRPN server.

The change was required to integrate these devices into the metadata-rich "path tree" model used by OSVR. Native OSVR device drivers provide not only access to the device data, but also a JSON "device descriptor" that describes the device capabilities and the semantic meaning of the numbered sensors. As standard VRPN servers lack this additional data, using a VRPN device in OSVR requires that the user supply this information, through the configuration file, to properly integrate the device.

As mentioned above, the methods described in this note require a version of OSVR-Core 0.2 or newer. Both client (application) DLLs and the server package must be 0.2 or newer, as that release broke protocol compatibility for metadata to enable greater functionality.

With regards to the external VRPN server, it may be any 07.XX release (newer preferred, particularly for button devices), running locally or remotely. (This support does includes devices that integrate the VRPN server as their native reporting protocol, as long as the protocol is version 7.)

If you run a vrpn_server process on the same machine as the OSVR server, you will have to pass an alternate port number (such as 3884) as a command line argument to vrpn_server to avoid collision with the embedded VRPN server in OSVR. Configuration of such an external server is beyond the scope of this document: it is assumed that you know the device name (often something like Tracker0) and the server (commonly just a hostname like trackerserver, but may be hostname and port localhost:3884 for the suggested local vrpn_server, or even include transport tcp://trackerserver:3884). These two parts are typically specified with the @ symbol separating them, e.g. Tracker0@localhost:3884.

For the purposes of this document, we'll assume you are using some device similar in functionality to a tracker wand or a Razer Hydra. That is, a device that provides tracking, button, and analog data on a single name. This process may be repeated to add any number of external VRPN device names to the system.

Tips

You may wish to consult or start with the osvr_server_config.externalvrpn.sample.json sample config file as you create your own. The documentation below walks through construction of this config file section by section.

A text editor with support for JSON can be very helpful in editing these configuration files, for syntax checking, code folding, and automatic indentation.

Setting Up the Configuration

Step 1: Importing and describing the VRPN device

The first step is to create the path tree node representing your external device, with the device name, server, and descriptor data. This is done in a top-level element of the config file (osvr_server_config.json by default) object called externalDevices. A sample excerpt showing this section follows.

  "externalDevices": {
    "/myExternalDevice": {
      "deviceName": "Tracker0",
      "server": "localhost:3884",
      "descriptor": /* can also provide a file path here, or this is the minimal */ {
        "interfaces": {
          "tracker": {},
          "button": {},
          "analog": {}
        }
      }
    }
  }

The key externalDevices refers to a JSON object, where each key is the "path" where a device node will be created, and the value is an object containing the information required to create that node. (In the case of a native OSVR device, this path would have two levels: first the plugin name then the device name, like /com_osvr_Multiserver/OSVRHackerDevKit0. Once this externalDevices section is set up, you'll be able to refer to your external device just as you would a native OSVR device.)

In the example above, we've arbitrarily chosen /myExternalDevice as the path. The deviceName key contains our VRPN device name (Tracker0), while the server key contains the server info (localhost:3884). The last element of the object, labeled descriptor, is the metadata that OSVR makes use of but that VRPN does not provide. On the most basic level, it can be what is shown here: just an embedded JSON object with an interfaces member, that contains an object with members for each interface type you want to access. Instead of an object, you could also supply a string, which would be interpreted as a filename containing a JSON device descriptor. This is useful particularly if you're using the same device in multiple places: you can share the device descriptor file separately from the server config.

Variations of the minimal descriptor embedded and described above is sufficient to follow the rest of these instructions and use your device in OSVR. As such, further explanation of the full device descriptor format is beyond the scope of this document. However, if you wish (for instance, if you might distribute your descriptor file), you can provide a fully-featured device descriptor like those embedded in OSVR plugins. Some examples of full device descriptors follow:

You may use the OSVR JSON Editor web app to help you compose this section: it is a single-page application that uses the device descriptor JSON Schema to automatically generate an editor interface.

Verifying your results

At this point, you may stop and test your config file. You should see a line resembling the following when you run osvr_server with your config file:

[OSVR Server] External devices found and parsed from config file.

Running the osvr_print_tree utility should also show you something like this:

[   DeviceElement] /myExternalDevice
                     - corresponds to Tracker0@localhost:3884
[InterfaceElement] /myExternalDevice/analog
[InterfaceElement] /myExternalDevice/button
[InterfaceElement] /myExternalDevice/tracker

If you see the DeviceElement lines, but not the InterfaceElement lines, then there was an issue parsing your descriptor and it didn't find an interfaces section. You might find more information in the output of osvr_server.

Step 2: Setting up aliases

The OSVR system strongly discourages the use of hardware-specific paths, and instead recommends your application use "semantic paths" that can be set up to point to different hardware resources on different systems. (In older versions of OSVR, you may have seen mention of "routes" - that is the old name for the same concept. The old routes-based config file syntax is deprecated, with the new aliases syntax preferred.) On devices with OSVR plugins, their device descriptor usually sets up a /semantic tree underneath the device node, and may also include an automaticAliases section that provides suggestions for global alias paths. Unless you've added all these features to the device descriptor you provided in step 1, you'll have to set up the appropriate aliases in the config file.

Common paths include:

There is also a convention of placing "controller" inputs (buttons, triggers, joysticks) under /controller, and further /controller/left and /controller/right when the inputs are so associated with a tracker. Fewer specific paths are "well-known" in this portion of the path tree, and you are encouraged to create and use other semantic paths as aliases (/actions/jump as an alias for /controller/a) in your application and configuration.

The aliases, like the external devices, are configured with a JSON object in the config file, this time under the key aliases. An excerpt might look like this:

  "aliases": {
    "/controller/trigger": "/myExternalDevice/analog/0",
    "/controller/a": "/myExternalDevice/button/0",
    "/me/head": {
      "rotate": {
        "axis": "-x",
        "degrees": 180
      },
      "child": {
        "changeBasis": {
          "x": "x",
          "y": "-z",
          "z": "-y"
        },
        "child": "/myExternalDevice/tracker/0"
      }
    },
    "/me/hands/right": {
      "rotate": {
        "axis": "-x",
        "degrees": 180
      },
      "child": {
        "changeBasis": {
          "x": "x",
          "y": "-z",
          "z": "-y"
        },
        "child": "/myExternalDevice/tracker/1"
      }
    }
  }

This example shows two different syntaxes. The simplest, used for analog and button devices, as well as for trackers that do not need any transformation applied to align with the OSVR global coordinate system, is to simply have the alias path to create as the key, and the path the alias points to as the string value. The first two entries in the example above take this form.

The second syntax (used by the second two entries) is more complex because it enables application of a transformation tree to tracker data. The key, as before, is the alias path to create. However, in this case, the value is a JSON object. This object can have arbitrary levels of nesting, with each internal level specified by the key child. The final level must terminate with a child key whose value is a string: the path that the alias transforms and points to. Each level may contain transformation objects (like changeBasis and rotate shown here) - see other example files and/or separate documentation on supported transformations.

In all cases, the alias terminates at a path under the device path you set up in step 1. The syntax is /devicename/interfacename/sensornumber (or occasionally /devicename/interfacename for all sensors). Of course, if you set up a device descriptor that contains semantic paths, you can reference those instead of the raw device path in this step.

Verifying your results

At this point, you can run osvr_server again with your config file. The server output will not change, but theh output of running osvr_print_tree will reflect the changes to the path tree that your aliases made. For the example aliases above, you'll see something like this:

[   DeviceElement] /myExternalDevice
                     - corresponds to Tracker0@localhost:3664
[InterfaceElement] /myExternalDevice/analog
[   SensorElement] /myExternalDevice/analog/0
[InterfaceElement] /myExternalDevice/button
[   SensorElement] /myExternalDevice/button/0
[InterfaceElement] /myExternalDevice/tracker
[   SensorElement] /myExternalDevice/tracker/1
[   SensorElement] /myExternalDevice/tracker/0
[    AliasElement] /controller/a
                     -> /myExternalDevice/button/0
[    AliasElement] /controller/trigger
                     -> /myExternalDevice/analog/0
[    AliasElement] /me/hands/right
                     -> {"child":{"changeBasis":{"x":"x","y":"-z","z":"-y"},"child":"/myExternalDevice/tracker/1"},"rotate":{"axis":"-x","degrees":180}}
[    AliasElement] /me/head
                     -> {"child":{"changeBasis":{"x":"x","y":"-z","z":"-y"},"child":"/myExternalDevice/tracker/0"},"rotate":{"axis":"-x","degrees":180}}

There are a few changes to notice. First, you'll see there are now AliasElement entries for each alias we configured, and the default settings of osvr_print_tree enable printing the target of each alias, whether a string or the compact version of your JSON transform. Additionally, each path representing a sensor that is a valid target of an alias that you've specified is now explicitly mentioned as a SensorElement. These sensor elements are automatically generated when resolving aliases. If you don't see a SensorElement you were trying to target, then you've likely made a mistake in specifying the corresponding alias.

As a further test of any tracker aliases you've set up, use the OSVR Tracker Viewer application (distributed separately from the core/server - click here for binary downloads for the tracker viewer). Run it with the -h command line argument to see how to specify which paths in the path tree you want to visualize. The application opens in the standard OSVR coordinate system, so you can verify that your transformations are correct.


  1. Ryan A. Pavlik, PhD is a senior software engineer at Sensics.