Chapter 9. HID over GATT

We use a Human Interface Device (HID) every time we interact with a desktop or laptop computer. Keyboards and mice are the most common types of HID device. HID’s original definition, uses the USB standard to send and receive data. The next generation of HID devices uses the Bluetooth standard to send and receive data.

The Generic Attribute Profile (GATT) describes how Bluetooth LE transfers data among devices. The HID over GATT Profile (HOGP) defines how to create input and output HID devices using BLE.

More information on the HID over GATT Profile can be found on the Bluetooth Developer Portal in the Technology Overview pages.

HOGP and BLEPeripheral

The BLEPeripheral library provides an additional BLEHIDPeripheral API to ease the creation of HOGP devices.

BLEPeripheral has the following HID APIs built in:

BLEKeyboard

Used for sending standard keyboard key presses

BLEMouse

Used for sending mouse movement and click inputs

BLEMultimedia

Used for sending multimedia key presses, such as volume up/down, mute/unmute

BLESystemControl

Used for sending system-control key presses, such as power and sleep

Users can also implement other custom HID types, such as a joystick, in their sketches if needed.

These HID types can be added to a BLEHIDPeripheral much in the same way that attributes (services, characteristics, descriptors) are added to a BLEPeripheral.

BLEHIDPeripheral extends the BLEPeripheral API by adding the HID Service shown in Table 9-1, along with its characteristics and descriptors. They are required by the HOGP specification.

Using HOGP devices also requires pairing/bonding. Pairing is required to encrypt communications between the peripheral and centra, so eavesdroppers can’t spy on your inputs (key strokes) using a BLE sniffer. The BLEHIDPeripheral API manages bonding data by storing it in the Arduino’s EEPROM so that it is persisted even when power is lost (unlike RAM).

During pairing, the BLE central and peripheral exchange encryption keys. BLEPeripheral uses Just Works pairing. Instead of using a device-specific pairing code, Just Works uses a Temporary Key (TK) that is 16 bytes in size, with all bytes set to zero. The TK is used to calculate a Short-Term Key (STK) to encrypt communications, which is followed by a process to exchange a Long-Term Key (LTK). Both the central and peripheral store the LTK so that communications can continue to be encrypted in the future.

More information on the BLE pairing process can be found at “Bluetooth Low Energy SMP Pairing”.

Table 9-1. HID Service (UUID 0x1812)
Attribute UUID Properties Notes

HID Service

0x1812

HID Information Characteristic

0x2a4a

read

HID Control Point Characteristic

0x2a4c

write without response

HID Report Characteristics

0x2a4d

read, notify

One per HID device type

HID Report Reference Descriptor

0x2908

One per HID device type

Volume Knob

To see how this works, let’s build a volume knob using a rotary encoder and Arduino. The volume knob will be compatible with iOS 8, Android 5, and OS X 10.10 devices. The volume knob will allow users to increase and decrease the volume on their paired device, as well as to mute and unmute it.

Hardware

The following hardware is required:

Wiring

The wiring for the Bluefruit LE module is the same as in previous chapters; see “Wiring Up the Adafruit Bluefruit LE Module” for more details.

To wire the rotary encoder, place it in the middle of the breadboard with the side that has two pins facing the top of the breadboard. Now connect the button pins of the rotary encoder by connecting a wire from the top-left pin to pin 5 on the Arduino, and connecting the top-right pin to ground. Then connect the rotary encoder side by connecting a wire from the bottom-left pin to pin 4 on the Arduino, the middle-bottom pin to ground, and the bottom-right pin to pin 3 on the Arduino.

When complete, the wiring will look as it does in Figure 9-1. See Figure 9-2 for a photo of the wire sketch.

mkbt 0901
Figure 9-1. Wiring diagram of HID over GATT volume knob sketch
mkbt 0902
Figure 9-2. HID over GATT Volume Knob sketch wired

Arduino Library Setup

We’ll be using the following Arduino libraries:

See “Installing the BLE Peripheral Library” for instructions on setting up the BLEPeripheral library if you haven’t already done so.

Setting up the Encoder library

Follow these steps to set up the Encoder library:

  1. Open the Arduino IDE.

  2. Go to Sketch → Include Library → Manage Libraries…

  3. Search for Encoder.

  4. Select Encoder by Paul Stoffregen.

  5. Click the Install button.

  6. Close the Library Manager using the Close button.

Testing the Rotary Encoder

The Encoder library includes a basic example sketch, as shown in Example 9-1. Let’s try it out:

  1. Go to File → Examples → Encoder → Basic.

  2. Update the pin numbers for the encoder to 3 and 4 to match the wiring we set up earlier.

  3. Load the sketch onto your Arduino.

  4. Open the Serial Monitor and ensure the baud rate is set to 9600.

  5. Turn the knob on the rotary encoder.

Example 9-1. Basic.ino sketch
/* Encoder Library - Basic Example
 * http://www.pjrc.com/teensy/td_libs_Encoder.html
 *
 * This example code is in the public domain.
 */

#include <Encoder.h> 1

// Change these two numbers to the pins connected to your encoder.
//   Best Performance: both pins have interrupt capability
//   Good Performance: only the first pin has interrupt capability
//   Low Performance:  neither pin has interrupt capability
Encoder myEnc(3, 4); 2
//   avoid using pins with LEDs attached

void setup() {
  Serial.begin(9600);
  Serial.println("Basic Encoder Test:");
}

long oldPosition  = -999;

void loop() {
  long newPosition = myEnc.read(); 3
  if (newPosition != oldPosition) { 4
    oldPosition = newPosition;
    Serial.println(newPosition);
  }
}
1

Include the Encoder library.

2

Create an Encoder instance, making sure to update the pin numbers to 3 and 4.

3

Read the encoder position.

4

Compare it to the previously read position. If the position has changed, store and print the new value.

The value will:

  • Increase when the knob is turned clockwise.

  • Decrease when the knob is turned counterclockwise.

Implementing the Volume Knob

Now that we are familiar with the rotary encoder API, let’s combine it with the BLEHIDPeripheral API to create a volume knob.

Let’s create a new sketch in the Arduino IDE: File → New.

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

Now we need to import the libraries we will be using at the start of the sketch.

#include <SPI.h>
#include <BLEHIDPeripheral.h>
#include <BLEMultimedia.h>

#include <Encoder.h>

SPI is needed by BLEHIDPeripheral, so it is imported first. We use BLEHIDPeripheral instead of BLEPeripheral because it provides an easier interface to build HIDD over GATT peripherals; it uses BLEPeripheral internally. The last import is the Encoder library we used earlier in the chapter.

Next, let’s define the constants to match the wiring we completed earlier.

#define BLE_REQ   10
#define BLE_RDY   2
#define BLE_RST   9

#define BUTTON_PIN 5

#define ENC_RIGHT_PIN 3
#define ENC_LEFT_PIN  4

The first three are used for the Adafruit Bluefruit LE board, followed by the encoder button pin and then the other encoder pins.

Now let’s create the object instances and variables we need for the rest of the sketch. We’ll need a BLEHIDPeripheral instance to create a BLE peripheral, a BLEMultimedia instance to send multimedia inputs to the central, an Encoder instance, and a variable to keep track of the button state.

BLEHIDPeripheral bleHIDPeripheral = BLEHIDPeripheral(BLE_REQ, BLE_RDY, BLE_RST);
BLEMultimedia bleMultimedia;

Encoder encoder(ENC_RIGHT_PIN, ENC_LEFT_PIN);

int buttonState;

Next, we move on to the setup function. We will configure the Serial object for output at 9600 baud. Set the BUTTON_PIN as an input in pullup mode. In pullup mode, the Arduino’s internal pullup resistor is used, so there is no need to wire an external one on the breadboard.

void setup() {
  Serial.begin(9600);

  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

We then need to set an initial value of 0 for the encoder instance by using the write method.

void setup() {
  // ...

  encoder.write(0);
}

Now we will configure the bleHIDPeripheral instance. We’ll set the local name that the peripheral advertises to HID Volume. Then we will add our bleMultimedia instances as a HID device. This will automatically create and add new HID Report Characteristics (UUID 0x2a4d) and an HID Report Reference Descriptor (UUID 0x2908) to the peripheral.

void setup() {
  // ...

  bleHIDPeripheral.setLocalName("HID Volume");
  bleHIDPeripheral.addHID(bleMultimedia);

  bleHIDPeripheral.begin();
}

The final items remaining in setup are to start the bleHIDPeripheral by calling its begin method, and to print a message to the Serial port to indicate that the HID peripheral is set up.

void setup() {
  // ...

  bleHIDPeripheral.begin();

  Serial.println(F("BLE HID Volume Knob"));
}

If you will be using the volume knob with an Android device, you will also need to include the following line before bleHIDPeripheral.begin() in setup:

  bleHIDPeripheral.setReportIdOffset(1);

The default report id offset is 0, though most Android devices do not work when it is used so the report id offset needs to be changed to 1.

Now let’s implement the loop function of the sketch. We first need to check if the peripheral has a central connected. If is does, we will print the central’s address to the serial port.

void loop() {
  BLECentral central = bleHIDPeripheral.central();

  if (central) {
    Serial.print(F("Connected to central: "));
    Serial.println(central.address());
  }
}

While the central is connected, we need to poll the button and encoder for input events. We call a function named pollInputs, which we will implement shortly.

void loop() {
  // ...

  if (central) {
    // ...

    while (bleHIDPeripheral.connected()) {
      pollInputs();
    }
  }
}

When the central disconnects, let’s also print a message to the serial port.

void loop() {
  // ...

  if (central) {
    // ...

    Serial.print(F("Disconnected from central: "));
    Serial.println(central.address());
  }
}

Now let’s implement the pollInputs function we called from loop earlier. We’ll have it call two functions: pollButton for the button and pollEncoder for the encoder.

void pollInputs() {
  pollButton();

  pollEncoder();
}

The pollButton function will read the current value of the button pin using the digitalRead function and compare it to the previously stored value in buttonState. If the value is different, the button has either been pressed or released, and the new value is stored. When pressed, the value will be LOW, so we can send the mute key value MMKEY_MUTE to the central using the write method on the bleMultimedia instance.

void pollButton() {
  int tempButtonState = digitalRead(BUTTON_PIN);

  if (tempButtonState != buttonState) {
    buttonState = tempButtonState;

    if (buttonState == LOW) {
      Serial.println(F("Mute"));
      bleMultimedia.write(MMKEY_MUTE);
    }
  }
}

In the pollEncoder, we first read the current value from the encoder. If it is non-zero, the encoder has been rotated. When the value is above 0, the encoder has been rotated clockwise, which corresponds to a volume-up action. We will map this to the volume-up key value MMKEY_VOL_UP, and again use the write method on bleMultimedia to send the value. When the encoder state is less than zero, it has been turned counterclockwise. This action will be mapped to the volume-down key MMKEY_VOL_DOWN. Then we need to reset the encoder’s current value to 0.

void pollEncoder() {
  int encoderState = encoder.read();

  if (encoderState != 0) {
    if (encoderState > 0) {
      Serial.println(F("Volume up"));
      bleMultimedia.write(MMKEY_VOL_UP);
    } else {
      Serial.println(F("Volume down"));
      bleMultimedia.write(MMKEY_VOL_DOWN);
    }
    encoder.write(0);
  }
}

We can’t use the absolute position of the encoder, as we have no way of knowing what the current volume level of the central is. Also, the HID APIs only allows us to send key-press events and not absolute volume levels.

Now you can load the sketch on your Arduino, and then follow the steps in Appendix A to pair the HID peripheral with your iOS, Android, or OS X device.

Once paired successfully, you will be able to control the volume of the device using the knob. You will also be able to toggle muting by pressing the button built into the rotary encoder.

You’ll notice that it will turn the volume up or down by more than a notch. We can add some debounce logic to correct this by polling the inputs every 100ms. Update the beginning of the sketch with the following items: a constant for the poll interval, INPUT_POLL_INTERVAL, and a variable named lastInputPollTime to store the last input poll time.

// ...

#define ENC_RIGHT_PIN 3
#define ENC_LEFT_PIN  4

#define INPUT_POLL_INTERVAL 100

// ...

int buttonState;
unsigned long lastInputPollTime = 0;

void setup() {

// ...

We can then modify the pollInputs function to check if the last poll time has been over 100ms. If it has, both the button and encoder inputs are polled. Then we update the lastInputPollTime variable. The millis function is used for time tracking, and returns the number of milliseconds the sketch has been running.

void pollInputs() {
  if (millis() - lastInputPollTime > INPUT_POLL_INTERVAL) {
    pollButton();

    pollEncoder();

    lastInputPollTime = millis();
  }
}

The HID peripheral can only be paired with a single device at a time, but we don’t yet have a way to clear the pairing data. Let’s update the sketch to add this feature. We’ll clear the pairing data if the button is pressed on start. This will clear the pairing information stored in the EEPROM.

void setup() {
  // ...

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  buttonState = digitalRead(BUTTON_PIN);

  if (buttonState == LOW) {
    Serial.println(F("BLE HID Peripheral - clearing bond data"));

    bleHIDPeripheral.clearBondStoreData();
  }

  encoder.write(0);

  // ...
}

To clear the bonding data, hold the rotary encoder button down, and then press the reset button on the Arduino Uno. You will see a message displayed on the Arduino IDE serial monitor when pairing the data has been cleared successfully.

Whenever you clear the bond store of the peripheral, be sure to remove the device from the previously paired device. Instructions for this can be found in Appendix A.

Note

If you encounter issues connecting or pairing with the volume knob peripheral, clear the bond store of the peripheral and unpair it from all previously paired peripherals.

See the HID_volume.ino example sketch included with the BLEPeripheral library for a full code listing.

Conclusion

In this chapter, we created an Arduino-based BLE volume knob that can be used with a BLE-equipped iOS or Android device, as well as a Mac. We used the HID over GATT APIs included with the BLEPeripheral library, and the PJRC.com Teensy Encoder Library.

You can use the other HID over GATT APIs included with the BLEPeripheral library to create other HOGP devices, such as a numeric keypad or joystick.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.158.47