---
title: "Building a Physical AirHorn Button with Web USB"
description: "Build a WebUSB powered AirHorn button! This tutorial shows you how to build a physical button to control Paul Kinlan's AirHorn using an Arduino, a momentary button, and WebUSB.  The guide includes code, wiring diagrams, and even 3D printing instructions for a custom case.  Make your own fun, interactive project today!\n"
slug: "Building-a-Physical-AirHorn-Button-with-Web-USB"
created: 2020-02-22T00:00:00Z
updated: 2020-02-22T00:00:00Z
tags:
  - "web-usb"
  - "iot"
hero_image: "/img/2020/02/airhorn_final.jpg"
ai_assisted: false
---

During the holiday season, I decided to experiment with building a physical button for
[Paul Kinlan’s][1] [AirHorn][2]. This blogpost provides the instructions and links so you can also
build your own [WebUSB][3] powered AirHorn Button.

What you Will Need
 - An Arduino Device. I used an [Arduino Nano 33 IoT][12].
 - A Momentary button or Switch. I used this [massive red button][8] from Pimoroni.
 - A 10k Ohm Resistor

# Getting Started

WebUSB is an API that securely provides access to USB devices from Web Pages. The API has been
around for a while, and hass been available in Chrome [since version 61][11].

I didn’t know anything about the API and the first step was to figure out how hard this would be.
Fortunately, Francois Beaufort wrote a handy [getting started guide][5].

## Hooking up a Button on the Arduino

The button was hooked up to the Arduino Board exactly as shown in the [Arduino Button Tutorial][6].
The implementation uses the [Arduino WebUSB library][10]. Make sure to follow the steps to setup
the library on your development computer.

The Arduino implementation is a slightly modified verson of the Arduino Button Tutorial to
implement something analog to `keydown` and `keyup` events.

When the pin voltage is `HIGH`, it means the button is pressed. When it is `LOW`, it means the
button is not pressed. To create the desired behaviour we want to check when the button state
changes from `LOW` to `HIGH`, meaning the button was pressed, and from `HIGH` to `LOW`, meaning
the button was released.

We send an `ON` message via the serial bus when the button is pressed and an `OFF` message when
it is released.

Here's what the code looks like:

```arduino
#include <WebUSB.h>

/**
 * Follow the instructions on https://github.com/webusb/arduino/ to install
 * the library and get the Arduino IDE to build and install it correctly.
 */
WebUSB WebUSBSerial(1 /* https:// */, "webusb-horn.firebaseapp.com");

#define Serial WebUSBSerial

const int ledPin = 13;
const int buttonPin = 2;
int previousButtonState = 0;

void setup() {
  while (!Serial) {
    ;
  }
  Serial.begin(9600);
  Serial.write("Sketch begins.\r\n> ");
  Serial.flush();
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
}

void loop() {
  if (Serial) {
    int buttonState = digitalRead(buttonPin);
    if (buttonState != previousButtonState) {
      if (buttonState == HIGH) {
        digitalWrite(ledPin, HIGH);
        Serial.write("ON\r\n");
      } else {
        digitalWrite(ledPin, LOW);
        Serial.write("OFF\r\n");
      }
      Serial.flush();
    }
    previousButtonState = buttonState;
    delay(10);
  }
}

```
On the JavaScript side, we need to connect to the Arduino and then listen to messages on the serial
interface. When an `ON` message is received, we start the AirHorn with `airhorn.start()`. When an
`OFF` message is received, we stop it with `airhorn.stop()`;

This is implemented in the `_loopRead` method in the code listing below. Check this [commit][9] to
see all changes made to AirHorn to make this work.
```js
const HardwareButton = function(airhorn) {
  this.airhorn = airhorn;
  this.decoder = new TextDecoder();
  this.connected = false;
  const self = this;
  this._loopRead = async function() {
    if (!this.device) {
      console.log('no device');
      return;
    }

    try {
      const result = await this.device.transferIn(2, 64);
      const command = this.decoder.decode(result.data);
      if (command.trim() === 'ON') {
        airhorn.start({loop: true});
      } else {
        airhorn.stop();
      }
      self._loopRead();
    } catch (e) {
      console.log('Error reading data', e);
    }
  };

  this.connect = async function() {
    try {
      const device = await navigator.usb.requestDevice({
        filters: [{'vendorId': 0x2341, 'productId': 0x8057}]
      });
      this.device = device;
      await device.open();
      await device.selectConfiguration(1);
      await device.claimInterface(0);
      await device.selectAlternateInterface(0, 0);
      await device.controlTransferOut({
        'requestType': 'class',
        'recipient': 'interface',
        'request': 0x22,
        'value': 0x01,
        'index': 0x00,
      });
      self._loopRead();
    } catch (e) {
      console.log('Failed to Connect: ', e);
    }
  };

  this.disconnect = async function() {
    if (!this.device) {
      return;
    }

    await this.device.controlTransferOut({
      'requestType': 'class',
      'recipient': 'interface',
      'request': 0x22,
      'value': 0x00,
      'index': 0x00,
    });
    await this.device.close();
    this.device = null;
  };

  this.init = function() {
    const buttonDiv = document.querySelector('#connect');
    const button = buttonDiv.querySelector('button');
    button.addEventListener('click', this.connect.bind(this));
    if (navigator.usb) {
      buttonDiv.classList.add('available');
    }
  };

  this.init();
};
```
This is how the result looks like:

<video controls loop poster="/img/2020/02/airhorn.jpg">
  <source src="/img/2020/02/airhorn.webm" type="video/webm; codecs=vp8">
  <source src="/img/2020/02/airhorn_x264.mp4" type="video/mp4; codecs=h264">
</video>

## [Optional] Building a case for our button

Finally, to make a nice packaging for the project, we can 3D print a box to fit our button and
the required electronics inside. The design I used is [available at Thingiverse][7], and it contains
both the box and a lid where the button can be fitted.

Here's an interesting video of the box being printed:

<video controls loop muted poster="/img/2020/02/airhorn_box.jpg">
  <source src="/img/2020/02/airhorn_box.webm" type="video/webm; codecs=vp8">
  <source src="/img/2020/02/airhorn_box.mp4" type="video/mp4; codecs=h264">
</video>

## Final Result
Finally, I used blue acrylic paint to give a nice color for the box. This is what the final result
looks like:

![Complete AirHorn box](/img/2020/02/airhorn_final.jpg "Complete AirHorn box")

[1]: https://twitter.com/Paul_Kinlan
[2]: https://airhorner.com/
[3]: https://wicg.github.io/webusb/
[5]: https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web
[6]: https://www.arduino.cc/en/tutorial/button
[7]: https://www.thingiverse.com/thing:4088197
[8]: https://shop.pimoroni.com/products/massive-arcade-button-with-led-100mm-red
[9]: https://github.com/andreban/airhorn/commit/299c8c1b4c1fd8a49b8db48a9add4864cc6259a3
[10]: https://github.com/webusb/arduino
[11]: https://developers.google.com/web/updates/2017/09/nic61
[12]: https://store.arduino.cc/arduino-nano-33-iot