WebUSB  Arduino

by Andrea Stagi

API for secure access to USB devices from web pages.

Let's plug our device!

A notification appears!

Let's click on it!

And in the website you can choose your device

The device's firmware itself must specify which URLs can access it.

This is similar to the CORS mechanism in HTTP

The URLs must be secured with HTTPS (except for localhost)

Talk is cheap.

Let's code!

Hardware requirements

I can't use my old Arduino UNO

So I bought an Arduino Leonardo

ATmega32u4

VS

ATmega328

ATmega32u4 has the ability to talk USB natively 

Software requirements

In Chrome enable "Experimental Web Platform Features" flag

Arduino IDE >= 1.6.11

Go to avr/cores/arduino/USBCore.h

Find the line

#define USB_VERSION 0x200

and change 0x200 to 0x210.

/* ..... */

#define MSC_SUBCLASS_SCSI	    0x06 
#define MSC_PROTOCOL_BULK_ONLY	    0x50 

#ifndef USB_VERSION
#define USB_VERSION 0x210
#endif

/* ..... */

Include WebUSB Arduino library

Our first firmware

#include <WebUSB.h>

const WebUSBURL URLS[] = {
  { 1, "astagi.github.io/webusb/" },
  { 0, "localhost:8000" },
};

const uint8_t ALLOWED_ORIGINS[] = { 1, 2 };

WebUSB WebUSBSerial(URLS, 2, 2, ALLOWED_ORIGINS, 2);

#define Serial WebUSBSerial

sketch.ino

typedef struct {
  uint8_t scheme;
  const char* url;
} WebUSBURL;

WebUSBURL Structure

WebUSB(
    const WebUSBURL* urls, 
    uint8_t numUrls, 
    uint8_t landingPage,
    const uint8_t* allowedOrigins,
    uint8_t numAllowedOrigins
);

WebUSB Constructor

You can now use it like the Serial interface to read and write!

#define Serial WebUSBSerial

void loop() {
  if (Serial && Serial.available()) {
    command = Serial.read();
    /* ELABORATE!! */
    Serial.flush();
  }
}

PluggableUSB.h

Arduino library to expose low-level USB functionalities

WebUSB::WebUSB(
    const WebUSBURL* urls, uint8_t numUrls, 
    uint8_t landingPage,
    const uint8_t* allowedOrigins, 
    uint8_t numAllowedOrigins)
	: PluggableUSBModule(2, 1, epType),
	  urls(urls), numUrls(numUrls), 
          landingPage(landingPage),
	  allowedOrigins(allowedOrigins), 
          numAllowedOrigins(numAllowedOrigins)
{
	epType[0] = EP_TYPE_BULK_OUT;
	epType[1] = EP_TYPE_BULK_IN;
	PluggableUSB().plug(this);
}

How Arduino announces WebUSB support

You must implement getDescriptor method

int WebUSB::getDescriptor(USBSetup& setup) {
    if (USB_BOS_DESCRIPTOR_TYPE == setup.wValueH) {
        if (setup.wValueL == 0 && setup.wIndex == 0) {
            int whole_size = 0;
            // SEND BOS DESCRIPTOR PREFIX

            // SEND LANDING PAGE VALUE
            if (USB_SendControl(0, &landingPage, 1) < 0)
                return -1;

            whole_size += 1;

            // SEND BOS DESCRIPTOR SUFFIX
            
            return whole_size;
        }
    }
    return 0;
}
const uint8_t BOS_DESCRIPTOR_PREFIX[] PROGMEM = {
  0x05,  // Length
  0x0F,  // Binary Object Store descriptor
  0x39, 0x00,  // Total length
  0x02,  // Number of device capabilities

  // WebUSB Platform Capability descriptor 
  // (bVendorCode == 0x01).
  0x18,  // Length
  0x10,  // Device Capability descriptor
  0x05,  // Platform Capability descriptor
  0x00,  // Reserved
  0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47,
  0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65,
  0x00, 0x01,  // Version 1.0
  0x01,  // Vendor request code
};

You need to implement setup() method

Manage WebUSB requests

#define WEBUSB_REQUEST_GET_ALLOWED_ORIGINS 0x01

if (setup.bRequest == 0x01 && setup.wIndex == WEBUSB_REQUEST_GET_ALLOWED_ORIGINS)
    {
        uint8_t allowedOriginsPrefix[] = {
            // Allowed Origins Header, bNumConfigurations = 1
            0x05, 0x00, 0x0c + numAllowedOrigins, 0x00, 0x01,
            // Configuration Subset Header, bNumFunctions = 1
            0x04, 0x01, 0x01, 0x01,
            // Function Subset Header, bFirstInterface = pluggedInterface
            0x03 + numAllowedOrigins, 0x02, pluggedInterface
        };
        if (USB_SendControl(
                0, &allowedOriginsPrefix, 
                sizeof(allowedOriginsPrefix)
            ) < 0)
            return false;
        return USB_SendControl(0, allowedOrigins, numAllowedOrigins) >= 0;
    }

#define

WEBUSB_REQUEST_GET_URL

0x02

else if (setup.bRequest == 0x01 && setup.wIndex == WEBUSB_REQUEST_GET_URL)
{
    if (setup.wValueL == 0 || setup.wValueL > numUrls)
        return false;
    const WebUSBURL& url = urls[setup.wValueL - 1];
    uint8_t urlLength = strlen(url.url);
    uint8_t descriptorLength = urlLength + 3;
    if (USB_SendControl(0, &descriptorLength, 1) < 0)
        return false;
    uint8_t descriptorType = 3;
    if (USB_SendControl(0, &descriptorType, 1) < 0)
        return false;
    if (USB_SendControl(0, &url.scheme, 1) < 0)
        return false;
    return USB_SendControl(0, url.url, urlLength) >= 0;
}

Implement getInterface()

int WebUSB::getInterface(uint8_t* interfaceCount) {
    *interfaceCount += 1; // uses 1 interface
    WebUSBDescriptor webUSBInterface = {
        D_INTERFACE(pluggedInterface, 2, 0xff, 0, 0),
        D_ENDPOINT(
            USB_ENDPOINT_OUT(pluggedEndpoint),
            USB_ENDPOINT_TYPE_BULK, 0x40, 0
        ),
        D_ENDPOINT(
            USB_ENDPOINT_IN (pluggedEndpoint + 1),
            USB_ENDPOINT_TYPE_BULK, 0x40, 0
        )
    };
    return USB_SendControl(
        0, &webUSBInterface, 
        sizeof(webUSBInterface)
    );
}

JS App

requestDevice()

 It must be called via a user gesture like a touch or mouse click

const filters = [
  { 'vendorId': 0x2341, 'productId': 0x8036 },
  { 'vendorId': 0x2341, 'productId': 0x8037 },
];

return navigator.usb.requestDevice (
  { 'filters': filters }
).then(
  device_ => device = device_;
);

For a complete list of vendor / products

 

http://www.linux-usb.org/usb.ids

the open() function

return this.device.open()
  .then(() => {
    if (device.configuration === null) {
      return device.selectConfiguration(1);
    }
  })
  .then(() => device.claimInterface(2))
  .then(() => device.controlTransferOut({
      'requestType': 'class',
      'recipient': 'interface',
      'request': 0x22, // CDC_SET_CONTROL_LINE_STATE
      'value': 0x01,
      'index': 0x02}))
  .then(() => {
    readLoop();
  });

Reading from device

let readLoop = () => {
  device.transferIn(5, 64).then(result => {
    console.log(result.data);
    readLoop();
  }, error => {
    console.log(error);
  });
};

Writing to the device

device.transferOut(4, data);

Demo time!

What about Android?

Another Nanpy?

from nanpy import ArduinoApi

a = ArduinoApi()
a.pinMode(13, a.OUTPUT)
a.digitalWrite(13, a.HIGH)

from nanpy import DallasTemperature

sensors = DallasTemperature(5)
n_sensors = sensors.getDeviceCount()

addresses = []

for i in range(n_sensors):
    addresses.append(sensors.getAddress(i))

sensors.setResolution(12)

while True:
    sensors.requestTemperatures()
    for i in range(n_sensors):
        temp = sensors.getTempC(i)

stagi.andrea@gmail.com

twitter: @4stagi
github: @astagi

WebUSB ♥ Arduino

By Andrea Stagi

WebUSB ♥ Arduino

WebUSB and Arduino playground

  • 4,579