RF Modules Bluetooth Remote Controls

How Does a Bluetooth Remote Control Work?

Most Bluetooth remote controls use Bluetooth Low Energy (BLE) and the HID over GATT profile, which is what we describe here.

Our initial aim was to create a universal HID over GATT BLE module which would pair with any suitable STB, but found that it was more reliable to create custom firmware for each type of remote control.

Introduction - So How Does it all Work?

This is deliberately quite a simplified view of the BLE world for this overview, but for a more technical detail, I recommend BlueTooth Low Energy, The Developer's Handbook by Robin Heydon.

When a remote control (RCU) pairs with an STB, the STB goes through a process of interrogating the RCU to read the structure of the GATT service table. This table is basically split into sections called services, with each service being able to provide specific types of information. If both ends of the BT connection understand a given type of service, it means that data related to that service can be passed from one end to the other.

For example, one service the RCU could have is the Battery Service. If the RCU and STB are paired, and the RCU then writes the battery charge level into the Battery Level attribute of the Battery Service, then the STB will receive this data and know how to handle it.

The steps below describe the above process in more detail.

Step 1 - Pair with the Remote Control

BLE pairing is basically the following process:

  • When a new STB is initially plugged in, it will scan for BLE devices. (If an STB is already paired, the RCU can usually be used to navigate to a menu to start a new scan when needed.)

  • The RCU then advertises itself, which may happen automatically if it's not yet paired, or may have to be manually started by pressing some button combination.

  • The STB should then discover the RCU, and will usually be able to identify it via information included in the advertising data.

  • The STB then connects to the RCU which is handled by the Genaric Access Protocol (GAP). This protocol manages key exchange, encryption and other security measures.

  • The STB will then usually interrogate the RCU's service table so that it can understand the capabilities of the remote and can then decide whether to bond.

  • Bonding is the establishment of a long-term relationship so that the whole pair-process does not need to take place every time the RCU is picked up.

To program a RedRat BT module to behave in the same way as the RCU, it is necessary to extract the service table from the RCU.
This is done by getting a RedRat BT module to go through the same initial steps as the STB.

Step 2 - Extract the GATT Service Table

Once the BT module has connected to the RCU, we use a tool called BlueJay to scan the whole GATT service table. The table below is from the Google Nexus player remote control (click on image for full table):

BlueJay with part of the Google Nexus service table. Click for the full table.

The full table lists a number of services, some of which are well known and others may be custom. Each service type has a UUID, but custom services are given their own UUIDs, which are usually 128-bit rather than the shorter 16-bit UUIDs for common services.

So how does the service table as a whole work? It was probably designed by some very clever people but nonetheless feels a bit clunky. Finding decent online documentation is also quite difficult, but maybe start here.

Going back to the battery service, this is the set of attributes read from the remote control:

Battery Service

which can be compared with the BT Battery Service specification.

If you look at the whole service table, an unknown or custom service type is given at the bottom. Without access to engineering information about the remote control, it's not possible to know what this service is. One could hazard a guess that it maybe used for transmission of audio data for voice control of the STB.

Step 3 - Process and Auto-generate BT Module Firmware

Now that we've read the full service table, this has to be programmed into the RedRat BT module so that it presents the same set of services to the STB, and so can then act as a remote control.

Initially we made the service table setup dynamic, so that it would be downloaded when the BT module booted. This actually led to a number of complications, particularly in ensuring that the BT module and application software remained in sync. For example, when attempting to pair a number of BT modules with a number of STBs, then those not required are generally unplugged to prevent accidental pairing. Each time a BT module is plugged back in, it has to be re-initialised before use, which is slow and error prone.

Therefore we found it to be considerably more reliable to create custom firmware for each different RCU that the BT module simulated:

// Nexus RCU service table
static char serviceTableData[] = {
        0x07, // Number of services
        0x03, 0x01, 0x00, 0x03, 0x00, // Att count, start & end handle
                // PrimaryService16
                0x08, 0x00, 0x01, 0x00, 0x00, 0x03, 0x01, 0x18,
                // CharacteristicDeclaration16
                0x09, 0x00, 0x02, 0x00, 0x05, 0x03, 0x20, 0x05,
                0x2A,
                // CharacteristicValue16
                0x0C, 0x00, 0x03, 0x00, 0x07, 0x03, 0x05, 0x2A,
                0x00, 0x00, 0x00, 0x00,
        0x07, 0x04, 0x00, 0x0A, 0x00, // Att count, start & end handle
                // PrimaryService16
                0x08, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x18,
                // CharacteristicDeclaration16
                0x09, 0x00, 0x05, 0x00, 0x05, 0x03, 0x02, 0x00,
                0x2A,
                // CharacteristicValue16
                0x19, 0x00, 0x06, 0x00, 0x07, 0x03, 0x00, 0x2A,
                0x0D, 0x00, 0x00, 0x00, 0x52, 0x65, 0x64, 0x52,
                0x61, 0x74, 0x20, 0x52, 0x65, 0x6D, 0x6F, 0x74,
                0x65,
                ...

This and other associated data structures are used by the firmware on startup to configure the BT stack appropriately.

Step 4 - Interpret the HID Descriptors

OK, so now we have out BT module potentially able to act as the Nexus Player remote control. But this profile is called HID over GATT, so where's the HID part?

The full HID service declaration is shown below, which should give enough information for any HID driver to understand the HID reports being sent.

Bluetooth documentation for the HID service, identified by UUID = 0x1812 is given here.

GATT HID service declaration

The first point of interest is the Report Map, highlighted by the red box above. This report map is also known as the HID descriptor is exactly the same as used by USB devices etc. In full, it is:

05 0C 09 01 A1 01 85 02 15 00 25 01 75 01 95 12 0A 42 00 0A 43 00
0A 44 00 0A 45 00 0A 41 00 0A CD 00 0A 23 02 0A 24 02 0A 21 02 81
02 95 01 75 07 81 03 C0 05 0C 09 01 A1 01 85 F7 95 14 75 08 15 00
26 FF 00 81 00 C0 05 0C 09 01 A1 01 85 FA 95 14 75 08 15 00 26 FF
00 81 00 C0 05 0C 09 01 A1 01 85 FB 95 14 75 08 15 00 26 FF 00 81
00 C0 05 0C 09 01 A1 01 85 F8 95 0B 75 08 15 00 26 FF 00 81 00 95
0B 75 01 15 00 26 FF 00 B1 00 C0 05 0C 09 01 A1 01 85 03 05 01 09
06 A1 02 05 06 09 20 15 00 26 64 00 75 08 95 01 81 02 C0 C0

Whoa, so what does that all mean? Help is online here, and when the above data is entered, the resulting HID descriptor is generated:

0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0x02,        //   Report ID (2)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x12,        //   Report Count (18)
0x0A, 0x42, 0x00,  //   Usage (Menu Up)
0x0A, 0x43, 0x00,  //   Usage (Menu Down)
0x0A, 0x44, 0x00,  //   Usage (Menu Left)
0x0A, 0x45, 0x00,  //   Usage (Menu Right)
0x0A, 0x41, 0x00,  //   Usage (Menu Pick)
0x0A, 0xCD, 0x00,  //   Usage (Play/Pause)
0x0A, 0x23, 0x02,  //   Usage (AC Home)
0x0A, 0x24, 0x02,  //   Usage (AC Back)
0x0A, 0x21, 0x02,  //   Usage (AC Search)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01,        //   Report Count (1)
0x75, 0x07,        //   Report Size (7)
0x81, 0x03,        //   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0xF7,        //   Report ID (-9)
0x95, 0x14,        //   Report Count (20)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0xFA,        //   Report ID (-6)
0x95, 0x14,        //   Report Count (20)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0xFB,        //   Report ID (-5)
0x95, 0x14,        //   Report Count (20)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0xF8,        //   Report ID (-8)
0x95, 0x0B,        //   Report Count (11)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x0B,        //   Report Count (11)
0x75, 0x01,        //   Report Size (1)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0xB1, 0x00,        //   Feature (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0,              // End Collection
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0x03,        //   Report ID (3)
0x05, 0x01,        //   Usage Page (Generic Desktop Ctrls)
0x09, 0x06,        //   Usage (Keyboard)
0xA1, 0x02,        //   Collection (Logical)
0x05, 0x06,        //     Usage Page (Generic Dev Ctrls)
0x09, 0x20,        //     Usage (Battery Strength)
0x15, 0x00,        //     Logical Minimum (0)
0x26, 0x64, 0x00,  //     Logical Maximum (100)
0x75, 0x08,        //     Report Size (8)
0x95, 0x01,        //     Report Count (1)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              //   End Collection
0xC0,              // End Collection

The most interesting part of this for us is Report ID 2 at the top, which gives details on the key report data sent when remote control buttons are pressed.

Interpreting HID report descriptors is a bit of an art, but some information is given here and here.

Fortunately, as BlueJay is still paired with the RCU, we can press buttons on the RCU and actually see the data appearing in the table. This is the line highlighted in blue in the service table, and has the Report Reference of 02, corresponding to the HID report ID of 2.

Notice also that there are a number of other HID report references in the GATT service declaration, with references of 0xFA, 0xFB, 0xF7 and 0xF8. The corresponding report IDs are also seen in the HID report descriptor, indicating that the Bluetooth chip/firmware in the RCU is capable of other types of HID comms, so could possibly used in game controllers or keyboards?

One last point on HID over GATT is that it is fully self describing, so theoretically any BLE STB supporting it should be able to work with any BLE RCU using HID over GATT. This would potentially allow us to create a generic RedRat Bluetooth device which could support a number of STBs.

Unfortunately, in practice we've found this not to be the case for at least two reasons:

  1. Some STBs won't pair with RCUs unless they see that it implements the full set of GATT services, including unknown/custom service definitions.
  2. Even when an RCU will pair with an STB, and the STB has the full HID descriptor, it may not actually support all HID key report messages, i.e. the STB's software only supports the HID reports it is expecting from the RCU.

Step 5 - Provide a Software API for Writing HID Messages

The BT module's firmware exposes some features of the BT stack up to the application software to control some aspects of GAP operation (pairing, connecting etc) and also support for sending GATT notifications.

Generally microcontroller firmware built on a BT stack is tightly coupled with the stack and so handles BT events and messages quickly. Initially, we exposed a number of the BT events and calls via the application API, having application code making the responses and so controlling operation. This did not work very reliably as the extra round-trip time for responding to BT events seemed to cause issues.

The guiding principle now is that the firmware should handle as much BT of the stack interaction as possible, with the API exposing actions to initiate higher-level operations and sending the GATT notifications used in sending HID reports (equivalent to pressing buttons on a remote control).

The button press HID report data captured through BlueJay requires the following fields (JSON format):

"KeyReports": [
    {
      "KeyName": "UP",
      "Handle": 44,
      "KeyDownReport": "01",
      "KeyUpReport": "00"
    },
    {
      "KeyName": "DOWN",
      "Handle": 44,
      "KeyDownReport": "02",
      "KeyUpReport": "00"
    },
    {
      "KeyName": "LEFT",
      "Handle": 44,
      "KeyDownReport": "04",
      "KeyUpReport": "00"
    },
    ...

With the following C# code used by application software for sending the equivalent of an RCU button press:

            try
            {
                BtModule.LockModule();

                var keyReportData = ConfigInfo
                    .KeyReports
                    .FirstOrDefault( r => r.KeyName.Equals( command, StringComparison.CurrentCultureIgnoreCase ) );

                if ( keyReportData == null )
                {
                    throw new Exception( $"Command '{command}' is not known." );
                }

                var report = press ? keyReportData.KeyDownReport : keyReportData.KeyUpReport;
                BtModule.SendGattNotification( targetId, keyReportData.Handle, report );
            }
            finally
            {
                BtModule.ReleaseModule();
            }