Interacting with USB HID devices from web apps

While building our cloud dictation portal, RedSpeak, we ran into a limitation for web apps: hardware support. One of the main interfaces for controlling a dictation audio player while transcribing is through foot switches. Transcribers need to be able to type and control the audio player at the same time with their feet (pause, rewind one or two words, change the playback rate, etc.).

Allowing the use of USB foot switches was therefore a must-have for us. Traditionally this would have meant building platform-specific daemons to catch the USB signals and forward them to the browser, probably in the form of keyboard shortcuts. But that would mean either building many platform-specific solutions, or not offering foot switch support on all platforms. Both big no-noes for our start-up.

Instead, we turned to Chrome’s App platform and its HID (Human Interface Device) API. This API is only available for apps (so not for extensions or web scripts) and gives them direct access to any USB HID device. Our platform runs in a browser as a website, so instead of wrapping it in an app completely, we decided to build a Chrome App that would just interact with our website – as a sort of device driver if you will. This posed yet new challenges of getting the Chrome App to interact with the website in this way.

In this tutorial, we’ll show you how we did it, and explain how you can use the Chrome App platform to write a cross-platform device driver for your web apps.

1. Some background

Since Chrome 26, Chrome Apps have been able to interact with USB devices. This is pretty awesome, but not always ideal, as operating systems may take ownership of the USB device before you can even start talking to it:

Not all devices can be accessed through the USB API. In general, devices are not accessible because either the Operating System’s kernel or a native driver holds them off from user space code. Some examples are devices with HID profiles on OSX systems, and USB pen drives.

Luckily for us, since Chrome 38 there is a HID api as well. HID devices form a subclass of USB devices, alongside mass-storage, printers, etc. and have a simple API because they only use the control and interrupt pipes of the USB interface. Our devices are simple HID devices, so we are covered!

You can figure out whether your device is a HID device or not via the command line. On *NIX find the device first in the results of

[bash]
$ lsusb [/bash]

and then find it again by Bus and Device in the results of

[bash]
$ lsusb -t [/bash]

To verify your device is a HID devices, look for Class=Human Interface Device.

One note: the HID api has changed a fair bit in Chrome 39, so we decided to support only Chrome >= 39.

2. Creating an app that interacts with a website

Our goal is to add Chrome API functionality to an existing web app (online only), so our first intuition was to simply create a content script and inject it into our app. But only Chrome Apps can get permissions to access USB devices, and content scripts are not available for apps – they are a concept from the Chrome Extensions platform.

So to inject the functionality into the web app, we needed to build a custom communication pipe between our web app and the Chrome app. We did this through Chrome’s message passing functionalities.

The gist is this: the script on the website checks if the user is running Chrome, and then tries to connect to the app. This wakes up the app, which starts listening for user input on the foot switch and sends an event to the website script if it does. The website script then decides what to do with the event.

2.1 Bare bones app

We first created a bare bones app, to make sure we had an app ID to work with. If you have never worked with Chrome Apps before, the Create Your First App guide from Google is a recommended primer.

We went ahead and gave our app permissions for some of the devices we were going to work with. The syntax is the following:

Using the lsusb command user earlier (on *NIX), you can find out the vendor and product id of your device. The manifest doesn’t accept the hexadecimal value you find there, so you need to convert it to decimal. You can quickly do this in your JavaScript console of choice with parseInt( 'hex_goes_here', 16 ).

Then, we also made sure that our domain and only our domain would be able to talk to our app, by adding it in the list of externally_connectable domains in the manifest:

[js]
// Check if the user is using Chrome if (typeof chrome !== ‘undefined’){

// Check if the app is installed and enabled by opening a messaging port var port = chrome.runtime.connect(‘laajpojboieljeaebebfefeimjpmellm’);

if (port) {

// When our app sends an event, it can safely be triggered on window port.onMessage.addListener(function(msg) { $(window).trigger(msg); });

}

} [/js]

To recap: our web script tries to connect to our new Chrome App, and when it does, it triggers every message the app sends as an event on window. The web script is set up so that it performs the appropriate action based on events on window.

Everything is now in place to start listening to the foot switch!

2.3 Waking up the app

Message passing in Chrome Apps is quite trivial. Above, we have set up the web script to simply open a port that we can use to send and receive messages on both sides. On the app side, we just need to listen for the opening of the port:

[js]
chrome.runtime.onConnectExternal.addListener(function (port) {

// The code here is run every time the web script on RedSpeak connects to the Chrome App. // This would be the moment to initialize the HID device.

For example, calling sendEvent('redspeak.play',port)from the app will make the audio player on the website start playing.

3. Interacting with the HID devices

OK! So we can now interact with the website from the app. Now for the fun part: interacting with the hardware. The workflow is as follows:

Getting the list of available devices

Connecting to the first available device

Continuously polling the device for events

Triggering an event when it happens

3.1 Connecting to the device

Step 1 is simple: chrome.hid.getDevices({},cb) passes all found devices to its callback. When we have the found devices, we can do some further checking on the devices and open the connection with the equally straight forward chrome.hid.connect(deviceId,cb).

Here is our full implementation:

[js]
function initializeHid(port) {

// We have defined all relevant devices in manifest.json. getDevices will // pass all devices we can interact with to the callback. If we want to // filter specific devices, you can specify them as the first argument chrome.hid.getDevices({}, function (devices) {

With a connection to both the USB HID device and the website, we can start polling the device for signals with the call to startPoller(port, connection, handler).

3.2 Poll the device

Receiving signals from a USB device is an active process, so we need to set up a loop that continuously polls the USB device. We set up the loop with an anonymous function that is called with setTimeout like this:

Hey presto, we now have an app that interacts with selected HID devices! It doesn’t do much, because we don’t have any real handlers for the devices yet, but with this scaffolding, we can start decoding device signals.

3.3 Decode device signals

Different devices return different signals, so let’s first connect one of the foot switches and listen to their signals. We start with the Philips foot switch :

As you can see, we can take the first value from the returned buffer when decoded as an array of unsigned 8-bit integers (make note). Trying all the buttons, on the Philips, we can map it to:

This Olympus switch has more buttons than the Philips, so we added changePlaybackRate as an extra feature.

3.4 Handle device signals

Now that we have a straight-forward mapping of device signals to app actions, we can detect the device type and set up a signal handler accordingly. Remember that the handler was created after the port was opened, but before we started polling the device.

Because different types of foot switches return different signals, we first determine the type in determineDeviceType(). The type returned is later used to decode the signal and then map the signal to an event.

To determine the device type, we use the same vendor and product ID as we used in manifest.json:

The app is now complete! The handler function is compiled and sent to the poller. When the poller receives data, it decodes it using these last few functions and passes the decoded message to the website script. In turn, the website script generates an event on window, which will trigger the desired effect on the audio player.

When bundling the app, you will get a new app ID, so make sure to change the app ID in the script on the website again if you are distributing a Chrome App like this. The key is based on the keypair you used to sign the package with. You can force the unpacked app to have the same key by adding the pubkey to your manifest. You can read how to do that here.

4. Wrapping up

This is all that’s needed to extend an online web-based app with extra hardware controls. The nice thing about interacting with a device through a JavaScript API is that in one fell swoop we have support for OSX and Linux, as well as all relevant Windows versions.

For us, this opens up a whole range of new opportunities. We can now develop our web app more like a desktop app, and consider interacting with Bluetooth devices (like headsets) or USB Mass Storage devices (like dictaphones).

How will you use the Chrome App platform? Let us know in the comments!

Please note, there is a typo/bug in the code posted. In the “initializeHid” function a call is made to the “startPoller” function with a second argument “connection”. The actual “startPoller” function listed expects a “connectionId” argument. To fix this I added this line to the “initializeHid” function just before the call to “startPoller”:

connectionId = connection.connectionId;

then changed the argument in that call from “connection” to “connectId”.

Hello, Don’t know if you are still taking comments with your work, but first off, thanks for sharing your work. I’m a bit new to javascript/chrome, but not new to USB development. My experience with communicating with USB devices is through VB6 and Windows API calls. I’m trying to learn from your code so that I can port my work to the Chrome environment. I’m using the https://developer.chrome.com/apps/hid website to understand how Chrome works with hid devices, but I was wondering if you might know of any other sources that dig a little deeper? I’ve modified your code to work with my USB device, and have scrolling Endpoint 1 data in the console. My next step is to place a button on the index.html page so that when I click it, I can read Endpoint 1 at that time and send the value to a text box. Any insight would be helpful! Thanks again.

Thanks for your comment! The most helpful resource for me was the Chrome dev docs. If you have the device working with my example code, you have the difficult part behind as far as interacting with the USB device goes. The next step would be to build a solid Chrome App – you may want to look at tutorials or resources about Chrome Apps in general for that (not much out there that focuses on USB / HID devices!).

For your app, you can either keep the code as is and keep the latest signal in a local variable, that you write to the input field when the button is clicked. Alternatively, you can remove poller, and only call `chrome.hid.receive` when the button is clicked and print the next result in the input field.