Chapter 66: BLE HID Device Implementation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamentals of the Human Interface Device (HID) protocol and its application over BLE (HID over GATT Profile – HOGP).
  • Identify the standard UUIDs for the HID Service and its essential characteristics.
  • Define a Report Map to describe the capabilities of a BLE HID device (e.g., keyboard, mouse).
  • Configure HID characteristics: HID Information, Report Map, HID Control Point, and Report characteristics.
  • Implement security measures typically required for HID devices, such as authenticated pairing.
  • Develop an ESP32 GATT server application that exposes an HID service, emulating a keyboard.
  • Send input reports from the ESP32 to a connected host (e.g., PC, smartphone).
  • Understand how to adapt the implementation for other HID devices like a mouse or consumer control device.

Introduction

Imagine cutting the cord on your keyboard or mouse, or creating custom wireless controllers for your projects. The Bluetooth Low Energy (BLE) Human Interface Device (HID) profile makes this possible. HID is a widely adopted standard that allows devices like keyboards, mice, gamepads, and remote controls to communicate their input data to host systems such as computers, smartphones, and gaming consoles.

When HID is implemented over BLE, it’s known as HID over GATT Profile (HOGP). This allows low-power wireless peripherals to seamlessly integrate with modern operating systems, which typically have built-in support for standard BLE HID devices. This means no special drivers are usually needed on the host side.

In this chapter, we will delve into the specifics of implementing a BLE HID device using an ESP32 and the ESP-IDF v5.x. We’ll focus on creating a simple BLE keyboard, demonstrating how to define its capabilities through a Report Map and send keystroke data to a connected host. This knowledge forms the foundation for creating a wide variety of custom wireless input devices.

Theory

HID Protocol Overview

The Human Interface Device (HID) protocol was originally defined for USB devices to provide a standardized way for input and output devices to communicate with host computers. Its core concept is the Report Descriptor, which describes the data packets (reports) that the device will send or receive. This descriptor tells the host what kind_of data the device provides (e.g., button presses, axis movements, LED states), how the data is formatted, its range, and its purpose.

Key HID Concepts:

  • Reports: Data packets exchanged between the HID device and the host.
    • Input Report: Data sent from the HID device to the host (e.g., key presses, mouse movements).
    • Output Report: Data sent from the host to the HID device (e.g., to control LEDs on a keyboard).
    • Feature Report: Data that can be read or written by the host, typically for configuration or status information (e.g., battery level if not using the standard Battery Service).
  • Report Descriptor: A structured piece of data that defines the format and meaning of all reports supported by the device. It’s composed of “items” that describe usages, logical and physical extents, report IDs, etc. The host parses this descriptor to understand how to interpret the reports from the device.
  • Usages: Standardized numerical codes that define the purpose of a control (e.g., “Keyboard Left Shift key”, “Mouse X-axis”, “Volume Up button”). These are defined in “Usage Tables” by the USB Implementers Forum (USB-IF).
Report Type Direction Purpose Example
Input Report Device to Host Transmits data from the HID device to the host system. Key presses from a keyboard, mouse movements, joystick axis changes.
Output Report Host to Device Transmits data from the host system to the HID device. Controlling LEDs on a keyboard (Caps Lock, Num Lock), force feedback to a joystick.
Feature Report Bidirectional (Host to Device or Device to Host) Used for configuration, status information, or device-specific features that are not part of standard input/output. Reading battery level (if not using Battery Service), setting device sensitivity, retrieving device serial number.

HID over GATT Profile (HOGP)

The HID over GATT Profile (HOGP) defines how the HID protocol is mapped onto the BLE GATT (Generic Attribute Profile) structure. This allows standard HID functionality over a low-power wireless link.

A HOGP device (like our ESP32) acts as a GATT server and exposes one or more instances of the HID Service.

HID Service (UUID: 0x1812)

This is the primary service for HOGP. A device can have multiple instances of the HID service if it logically groups HID functionalities (e.g., a combo keyboard/mouse device might expose them as separate HID instances, though often they are combined into one complex report descriptor).

The HID Service includes several mandatory and optional characteristics:

  1. HID Information Characteristic (UUID: 0x2A4A)
    • Properties: Read
    • Purpose: Provides basic information about the HID device, such as the HID version it complies with, and country code for localization (e.g., keyboard layout).
    • Value: A 4-byte value:
      • bcdHID (2 bytes): HID Class Specification release number in binary-coded decimal (e.g., 0x0111 for v1.11).
      • bCountryCode (1 byte): Hardware country code. 0 means not localized.
      • Flags (1 byte):
        • Bit 0: RemoteWake – 1 if device can send wake signal to host.
        • Bit 1: NormallyConnectable – 1 if device will initiate bonding when connected to a new host.
  2. Report Map Characteristic (UUID: 0x2A4B)
    • Properties: Read
    • Purpose: Contains the HID Report Descriptor. This is crucial as it defines the device’s functionality to the host.
    • Value: The raw byte array of the Report Descriptor. The host reads this once after connection to understand how to interpret subsequent reports.
    • Note: An optional “External Report Reference Descriptor” (UUID 0x2907) can be associated if parts of the report map are defined in other services (e.g., Battery Level in BAS).
  3. HID Control Point Characteristic (UUID: 0x2A4C)
    • Properties: WriteWithoutResponse
    • Purpose: Allows the host to send commands to the HID device.
    • Value:
      • 0x00: Suspend command (device should stop sending reports and enter low power state).
      • 0x01: Exit Suspend command (device should resume normal operation).
      • Other values are reserved.
  4. Report Characteristic (UUID: 0x2A4D)
    • Properties: Read, Write, WriteWithoutResponse, Notify (Input Reports), Indicate (Input Reports)
    • Purpose: Used to exchange Input, Output, and Feature reports.
    • Instances: A HID Service will have one or more Report characteristics. Each instance is distinguished by:
      • Report ID: If the Report Map defines multiple reports of the same type (e.g., multiple input reports), each report will have a unique ID. This ID is prepended to the actual report data if it’s non-zero.
      • Report Type: Defined by a “Report Reference Descriptor” (UUID 0x2908) associated with each Report characteristic.
        • 0x01: Input Report
        • 0x02: Output Report
        • 0x03: Feature Report
    • Input Reports: Typically support Read and Notify (or Indicate). The device sends data to the host. A CCCD (Client Characteristic Configuration Descriptor, UUID 0x2902) is required if Notify/Indicate is supported.
    • Output Reports: Typically support Read, Write, and WriteWithoutResponse. The host sends data to the device.
    • Feature Reports: Typically support Read and Write. Used for configuration data.
  5. Protocol Mode Characteristic (UUID: 0x2A4E)
    • Properties: Read, WriteWithoutResponse
    • Purpose: Used to switch between “Boot Protocol” mode and “Report Protocol” mode.
      • Boot Protocol Mode (0x00): A simplified mode for system startup (BIOS) where complex Report Descriptors might not be parsable. Defines fixed-format reports for basic keyboards and mice.
      • Report Protocol Mode (0x01): The default mode using the full Report Descriptor defined in the Report Map characteristic.
    • Presence: Mandatory if the device supports the boot protocol for any of its interfaces (e.g., boot keyboard or boot mouse).
  6. Boot Keyboard Input Report Characteristic (UUID: 0x2A22)
    • Properties: Read, Notify
    • Purpose: Used to send keyboard input reports when in Boot Protocol mode. Fixed format.
    • Presence: Mandatory if the device supports the boot keyboard role. Requires CCCD.
  7. Boot Keyboard Output Report Characteristic (UUID: 0x2A32)
    • Properties: Read, Write, WriteWithoutResponse
    • Purpose: Used to receive keyboard output reports (e.g., LED status) when in Boot Protocol mode. Fixed format.
    • Presence: Mandatory if the device supports the boot keyboard role.
  8. Boot Mouse Input Report Characteristic (UUID: 0x2A33)
    • Properties: Read, Notify
    • Purpose: Used to send mouse input reports when in Boot Protocol mode. Fixed format.
    • Presence: Mandatory if the device supports the boot mouse role. Requires CCCD.
Characteristic Name UUID Typical Properties Brief Purpose
HID Information 0x2A4A Read Provides HID version and country code.
Report Map 0x2A4B Read Contains the HID Report Descriptor defining device capabilities. Optionally uses External Report Reference Descriptor (0x2907).
HID Control Point 0x2A4C WriteWithoutResponse Allows host to send commands (Suspend, Exit Suspend) to the device.
Report 0x2A4D Read, Write, WriteWithoutResponse, Notify, Indicate Exchanges Input, Output, and Feature reports. Each instance has a Report Reference Descriptor (0x2908) defining its type (Input/Output/Feature) and ID. Input reports often require CCCD (0x2902).
Protocol Mode 0x2A4E Read, WriteWithoutResponse Switches between Boot Protocol (0x00) and Report Protocol (0x01) modes. Mandatory if boot protocol is supported.
Boot Keyboard Input Report 0x2A22 Read, Notify Fixed-format keyboard input in Boot Protocol mode. Mandatory if boot keyboard role supported. Requires CCCD.
Boot Keyboard Output Report 0x2A32 Read, Write, WriteWithoutResponse Fixed-format keyboard output (LEDs) in Boot Protocol mode. Mandatory if boot keyboard role supported.
Boot Mouse Input Report 0x2A33 Read, Notify Fixed-format mouse input in Boot Protocol mode. Mandatory if boot mouse role supported. Requires CCCD.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    subgraph "HID Service (0x1812)"
        direction LR
        INFO["HID Information<br>(0x2A4A)<br><br>Properties: Read"]
        RMAP["Report Map<br>(0x2A4B)<br><br>Properties: Read"]
        CP["HID Control Point<br>(0x2A4C)<br><br>Properties: WriteWithoutResponse"]
        
        subgraph REPORTS["Report Characteristics (0x2A4D)"]
            direction TB
            REP_IN["Input Report 1<br>(0x2A4D)<br><br>Properties: Read, Notify<br>Ref: Type Input (0x01), ID X"]
            REP_OUT["Output Report 1<br>(0x2A4D)<br><br>Properties: Read, Write, WriteWoResp<br>Ref: Type Output (0x02), ID Y"]
            REP_FEAT["Feature Report 1<br>(0x2A4D)<br><br>Properties: Read, Write<br>Ref: Type Feature (0x03), ID Z"]
            REP_MORE["..."]
        end

        PMODE["Protocol Mode<br>(0x2A4E)<br><br>Properties: Read, WriteWoResp<br>(Mandatory if Boot Supported)"]
        
        subgraph BOOT_REPORTS["Boot Protocol Reports (Optional)"]
            direction TB
            BOOT_KB_IN["Boot Keyboard Input Report<br>(0x2A22)<br><br>Properties: Read, Notify"]
            BOOT_KB_OUT["Boot Keyboard Output Report<br>(0x2A32)<br><br>Properties: Read, Write, WriteWoResp"]
            BOOT_MOUSE_IN["Boot Mouse Input Report<br>(0x2A33)<br><br>Properties: Read, Notify"]
        end

        RMAP --- REP_IN
        RMAP --- REP_OUT
        RMAP --- REP_FEAT

        INFO -.-> HID_SERVICE
        RMAP -.-> HID_SERVICE
        CP -.-> HID_SERVICE
        REPORTS -.-> HID_SERVICE
        PMODE -.-> HID_SERVICE
        BOOT_REPORTS -.-> HID_SERVICE
    end

    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef optional fill:#FEFCE8,stroke:#A16207,stroke-width:1px,color:#713F12;
    classDef characteristic_group fill:#F0F9FF,stroke:#0284C7,stroke-width:1px,color:#075985;

    class HID_SERVICE primary;
    class INFO,RMAP,CP,PMODE process;
    class REPORTS,BOOT_REPORTS characteristic_group;
    class REP_IN,REP_OUT,REP_FEAT,REP_MORE,BOOT_KB_IN,BOOT_KB_OUT,BOOT_MOUSE_IN process;

    style REPORTS fill:#transparent,stroke:#ccc,stroke-dasharray: 5 5;
    style BOOT_REPORTS fill:#transparent,stroke:#ccc,stroke-dasharray: 5 5;

Report Descriptors Explained

The Report Descriptor is the heart of a HID device. It’s a sequence of bytes that defines the data exchanged with the host. It’s like a blueprint for the device’s input and output capabilities. Writing report descriptors can be complex and error-prone. Tools like the USB-IF’s HID Descriptor Tool can be helpful.

A Report Descriptor is built from items. Items can be short or long. Short items consist of a 1-byte prefix (size, type, tag) and 0, 1, 2, or 4 data bytes.

Common Item Types:

Item Category Item Type Tag (Prefix Bits) Brief Description
Main Items
(Define or group report fields)
Input 100000nn (0x80) Defines data fields sent from the HID device to the host.
Output 100100nn (0x90) Defines data fields sent from the host to the HID device.
Feature 101100nn (0xB0) Defines data fields for configuration or status information, readable/writable by the host.
Collection 101000nn (0xA0) Groups related items into a logical unit (e.g., Application, Physical, Logical).
End Collection 110000nn (0xC0) Marks the end of a collection group.
Global Items
(Affect subsequent items until changed)
Usage Page 000001nn (0x04) Sets the context for subsequent Usage items (e.g., Generic Desktop, Keyboard/Keypad).
Logical Minimum / Maximum 000101nn (0x14) / 001001nn (0x24) Defines the range of possible values for a control.
Report Size 011101nn (0x74) Specifies the size of each field in bits.
Report Count 100101nn (0x94) Specifies the number of fields described by the current item.
Report ID 100001nn (0x84) Assigns an ID to a report, allowing multiple reports of the same type (Input, Output, Feature).
Local Items
(Affect only the next Main item)
Usage 000010nn (0x08) Defines the specific function of a control within the current Usage Page.
Usage Minimum / Maximum 000110nn (0x18) / 001010nn (0x28) Defines a range of usages for an array of similar controls.
(Other local items like Designator, String, Delimiter exist) Various Provide more detailed descriptions for the next Main item.
  • Main Items: Define report fields or collections.
    • Input (Tag 0x80): Defines data fields sent from device to host.
    • Output (Tag 0x90): Defines data fields sent from host to device.
    • Feature (Tag 0xB0): Defines data fields for configuration/status.
    • Collection (Tag 0xA0): Groups related items (e.g., an application collection for a mouse).
    • End Collection (Tag 0xC0): Ends a collection.
  • Global Items: Affect subsequent items.
    • Usage Page (Tag 0x04): Sets the context for usages (e.g., Generic Desktop, Keyboard/Keypad).
    • Logical Minimum/Maximum (Tag 0x14, 0x24): Defines the range of values for a control.
    • Report Size (Tag 0x74): Size of each field in bits.
    • Report Count (Tag 0x94): Number of fields.
    • Report ID (Tag 0x84): Assigns an ID to a report, allowing multiple reports of the same type.
  • Local Items: Affect the next main item.
    • Usage (Tag 0x08): Defines the specific function of a control within the current Usage Page.
    • Usage Minimum/Maximum (Tag 0x18, 0x28): Defines a range of usages for an array of controls.

Example: Simple Keyboard Report Descriptor Snippet

This is a highly simplified conceptual view. Actual descriptors are byte arrays.

Plaintext
Usage Page (Generic Desktop)      ; 0x05, 0x01
Usage (Keyboard)                  ; 0x09, 0x06
Collection (Application)          ; 0xA1, 0x01
  Report ID (1)                   ; 0x85, 0x01 (If using report IDs)
  Usage Page (Keyboard/Keypad)    ; 0x05, 0x07
  Usage Minimum (Keyboard LeftControl) ; 0x19, 0xE0
  Usage Maximum (Keyboard Right GUI)   ; 0x29, 0xE7
  Logical Minimum (0)             ; 0x15, 0x00
  Logical Maximum (1)             ; 0x25, 0x01
  Report Size (1)                 ; 0x75, 0x01 (1 bit per modifier key)
  Report Count (8)                ; 0x95, 0x08 (8 modifier keys)
  Input (Data, Variable, Absolute); 0x81, 0x02 (1 byte for modifiers)

  Report Size (8)                 ; 0x75, 0x08 (8 bits per key)
  Report Count (6)                ; 0x95, 0x06 (Can report up to 6 simultaneous key presses)
  Logical Minimum (0)             ; 0x15, 0x00
  Logical Maximum (101)           ; 0x25, 0x65 (Typical number of key codes)
  Usage Page (Keyboard/Keypad)    ; 0x05, 0x07 (Redundant if not changed, but good practice)
  Usage Minimum (Reserved)        ; 0x19, 0x00
  Usage Maximum (Keyboard Application); 0x29, 0x65
  Input (Data, Array, Absolute)   ; 0x81, 0x00 (6 bytes for key codes)
End Collection                    ; 0xC0

The actual byte array for a report descriptor is what you provide in the Report Map characteristic.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    subgraph REPORT_DESCRIPTOR [Report Descriptor for a Keyboard Key]
        direction TB

        UP_GENERIC[("Usage Page (Generic Desktop: 0x01)")]:::global
        USAGE_KEYBOARD[("Usage (Keyboard: 0x06)")]:::local
        COLLECTION_APP[("Collection (Application)")]:::main

        subgraph KEY_DEFINITION ["Defining a Single Key Press Report (e.g., 'A' key)"]
            direction TB
            UP_KEYPAD[("Usage Page (Keyboard/Keypad: 0x07)")]:::global
            LOG_MIN_0[("Logical Minimum (0)")]:::global
            LOG_MAX_1[("Logical Maximum (1)")]:::global
            REP_SIZE_1[("Report Size (1 bit)")]:::global
            REP_COUNT_1[("Report Count (1 field)")]:::global
            USAGE_A[("Usage (Keyboard a and A: 0x04)")]:::local
            INPUT_KEY["Input (Data, Variable, Absolute)"]:::main_input
        end

        subgraph MODIFIER_BYTE ["Defining Modifier Byte (Conceptual)"]
             direction TB
             UP_KEYPAD2[("Usage Page (Keyboard/Keypad: 0x07)")]:::global
             USAGE_MIN_CTRL[("Usage Minimum (Left Control: 0xE0)")]:::local
             USAGE_MAX_GUI[("Usage Maximum (Right GUI: 0xE7)")]:::local
             LOG_MIN2_0[("Logical Minimum (0)")]:::global
             LOG_MAX2_1[("Logical Maximum (1)")]:::global
             REP_SIZE2_1[("Report Size (1 bit per modifier)")]:::global
             REP_COUNT2_8[("Report Count (8 modifiers)")]:::global
             INPUT_MOD["Input (Data, Variable, Absolute) - 1 Byte for Modifiers"]:::main_input
        end
        
        COLLECTION_APP_END[("End Collection")]:::main

        UP_GENERIC --> USAGE_KEYBOARD
        USAGE_KEYBOARD --> COLLECTION_APP
        COLLECTION_APP --> MODIFIER_BYTE
        MODIFIER_BYTE --> KEY_DEFINITION
        KEY_DEFINITION --> COLLECTION_APP_END
        
        %% Styling for items
        MODIFIER_BYTE --> INPUT_MOD
        UP_KEYPAD2 --> USAGE_MIN_CTRL 
        USAGE_MIN_CTRL --> USAGE_MAX_GUI
        USAGE_MAX_GUI --> LOG_MIN2_0
        LOG_MIN2_0 --> LOG_MAX2_1
        LOG_MAX2_1 --> REP_SIZE2_1
        REP_SIZE2_1 --> REP_COUNT2_8
        REP_COUNT2_8 --> INPUT_MOD

        KEY_DEFINITION --> INPUT_KEY
        UP_KEYPAD --> LOG_MIN_0
        LOG_MIN_0 --> LOG_MAX_1
        LOG_MAX_1 --> REP_SIZE_1
        REP_SIZE_1 --> REP_COUNT_1
        REP_COUNT_1 --> USAGE_A
        USAGE_A --> INPUT_KEY
    end

    classDef global fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef local fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef main fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef main_input fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef structure fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3;

    class REPORT_DESCRIPTOR structure;
    class KEY_DEFINITION structure;
    class MODIFIER_BYTE structure;

Security

HID devices often transmit sensitive information (keystrokes). Therefore, HOGP mandates security:

  • LE Secure Connections pairing is highly recommended.
  • Authenticated pairing (e.g., Passkey Entry, or Numeric Comparison if both devices have displays) is typically required for keyboards to prevent eavesdropping or injection attacks. “Just Works” pairing may be acceptable for simpler devices like a presentation clicker, but offers no protection against MITM.
  • The ESP-IDF HID component helps manage security requirements. Connections are typically encrypted after successful pairing/bonding.
Security Feature/Method Description Typical HID Requirement ESP-IDF Configuration Aspect
LE Secure Connections (LESC) Pairing Enhanced pairing method using Elliptic Curve Diffie-Hellman (ECDH) key exchange for stronger security against passive eavesdropping and Man-in-the-Middle (MITM) attacks. Highly recommended for all HOGP devices. Enabled by default in modern ESP-IDF BLE stacks. Auth request flag ESP_LE_AUTH_REQ_SC_ONLY or ESP_LE_AUTH_REQ_SC_BOND or ESP_LE_AUTH_REQ_SC_MITM_BOND.
Authenticated Pairing (MITM Protection) Requires verification of the connecting parties to prevent MITM attacks. Achieved via Passkey Entry, Numeric Comparison, or Out-of-Band (OOB) data. Typically required for keyboards and devices transmitting sensitive data. Auth request flag includes MITM (e.g., ESP_LE_AUTH_REQ_SC_MITM_BOND). Requires setting appropriate I/O Capabilities (ESP_BLE_SM_IOCAP_MODE).
Passkey Entry One device displays a 6-digit passkey, and the user enters it on the other device. Suitable if one device has a display and the other has a keyboard. I/O Capabilities: ESP_IO_CAP_OUT (display) and ESP_IO_CAP_IN or ESP_IO_CAP_IO (keyboard). Handle ESP_GAP_BLE_PASSKEY_REQ_EVT or ESP_GAP_BLE_PASSKEY_NOTIF_EVT.
Numeric Comparison Both devices display a 6-digit number, and the user confirms if they match. Suitable if both devices have a display and at least one has a Yes/No confirmation capability. I/O Capabilities: e.g., ESP_IO_CAP_IO on both, or ESP_IO_CAP_OUT and ESP_IO_CAP_IN. Handle ESP_GAP_BLE_NC_REQ_EVT.
“Just Works” Pairing Simplest pairing method, no user interaction required for authentication. Offers no MITM protection. Uses Temporary Key (TK) value of zero. May be acceptable for low-security devices (e.g., simple presentation clicker) but not for keyboards. I/O Capabilities: ESP_IO_CAP_NONE on both devices, and auth request without MITM (e.g., ESP_LE_AUTH_REQ_BOND or ESP_LE_AUTH_REQ_SC_BOND if SC is still desired without MITM).
Bonding Storing the encryption keys (Long Term Key – LTK) after successful pairing to allow for faster, secure reconnections without repeating the full pairing process. Generally required for convenience and persistent security. Auth request flag includes BOND (e.g., ESP_LE_AUTH_REQ_SC_MITM_BOND). NVS flash must be initialized to store bonding information.
Encryption Data exchanged over the BLE link is encrypted using AES-CCM after successful pairing/bonding. Mandatory for secure HID communication. Automatically enabled by the BLE stack after successful secure pairing. Can be explicitly requested via esp_ble_set_encryption().

Operation Flow (ESP32 as HID Device)

  1. Initialization:
    • Initialize NVS, Bluetooth controller, and Bluedroid/Blufi stack.
    • Register GAP and GATTS (specifically HID profile) callbacks.
  2. Security Setup:
    • Configure authentication requirements (e.g., ESP_LE_AUTH_REQ_SC_MITM_BOND).
    • Set I/O capabilities (e.g., ESP_IO_CAP_NONE for a simple keyboard, ESP_IO_CAP_IO if it has a display/keyboard for passkey entry).
  3. HID Service Configuration:
    • The ESP-IDF esp_hidd_prf_api.h provides functions to initialize the HID device profile. This internally creates the HID service and its characteristics.
    • You provide the HID Information, Report Map, and define the number and types of Report characteristics.
  4. Advertising:
    • Set the device name.
    • Include the HID Service UUID (0x1812) in the advertising data.
    • Set appearance to a HID type (e.g., keyboard 0x03C1, mouse 0x03C2).
    • Start advertising.
  5. Connection & Pairing:
    • A host (PC, smartphone) scans and connects.
    • The BLE stack handles pairing and bonding according to the configured security parameters. The user might need to confirm a passkey or comparison.
  6. Service Discovery by Host:
    • The host discovers services and characteristics, reads the Report Map to understand the device.
    • The host subscribes to notifications for Input Reports by writing to their CCCDs.
  7. Sending Reports:
    • When an event occurs on the ESP32 (e.g., a button is pressed), the application formats an Input Report according to the Report Map.
    • The application uses esp_hidd_send_input_report() to send the report to the connected and subscribed host.
  8. Receiving Reports (Optional):
    • If the device supports Output Reports (e.g., for keyboard LEDs), the host can send them. The ESP32 application will receive an event and can act on the data.
  9. Disconnection:
    • The device can re-advertise or enter a low-power state.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    A["Start: ESP32 Power On"]:::primary --> B{"Initialize NVS Flash"};
    B -- Success --> C{"Initialize BT Controller & Bluedroid/Blufi"};
    C -- Success --> D["Register GAP & GATTS (HID Profile) Callbacks"];
    D -- Success --> E["Security Setup: Auth Req & I/O Capabilities"];
    E -- Success --> F["HID Service Configuration: esp_hidd_prf_api.h<br>Set HID Info, Report Map, Report Chars"];
    F -- Success --> G["Configure Advertising Data: Name, HID Service UUID (0x1812), Appearance"];
    G -- Success --> H["Start Advertising"];
    
    H --> I{"Host Scans & Discovers ESP32"};
    I --> J["Host Initiates Connection"];
    J --> K{"Connection Established"};
    K --> L["Pairing & Bonding Process<br>(Security as per config: Passkey, NC, etc.)"];
    L -- Success --> M{"Secure Encrypted Link"};
    M --> N["Host Discovers Services & Characteristics"];
    N --> O["Host Reads Report Map Characteristic"];
    O --> P["Host Subscribes to Input Report CCCD (Enables Notifications)"];
    
    P --> Q{"User Action on ESP32<br>(e.g., Button Press)"};
    Q --> R["Application Formats Input Report<br>(Based on Report Map)"];
    R --> S["Call esp_hidd_send_input_report()"];
    S -- "Report Sent via Notification" --> P;

    subgraph "Normal Operation"
        P
    end

    K --> T_DIS1{"Host or Device Initiates Disconnect"};
    P --> T_DIS2{"Link Lost / Disconnect"};
    T_DIS1 --> U["Disconnection Event"];
    T_DIS2 --> U;
    U --> H_RE_ADV["Re-advertise or Low Power State"];
    
    subgraph "Host Interaction"
        direction LR
        I; J; N; O; P;
    end

    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef io fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef state fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3;

    class A primary;
    class B,C,D,E,F,G,H,K,L,M,N,O,P,Q,R,S,U,H_RE_ADV process;
    class I,J io;
    class Loop state;
    class HOST_INTERACTION state;
    class T_DIS1,T_DIS2 decision;

Practical Examples

Prerequisites:

  • An ESP32 board (ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2).
  • VS Code with the Espressif IDF Extension.
  • A host device (PC, Mac, Linux, Android, iOS) to test the BLE HID functionality.

Example 1: ESP32 as a BLE HID Keyboard

This example demonstrates how to configure the ESP32 to act as a simple BLE keyboard. It will send a predefined sequence of characters when a specific event occurs (for simplicity, we’ll trigger it via a task, but in a real device, it would be a button press).

1. Project Setup:

  • Create a new ESP-IDF project: idf.py create-project hid_keyboard_example
  • cd hid_keyboard_example
  • idf.py menuconfig:
    • Component config -> Bluetooth -> Bluetooth (Enable)
    • Component config -> Bluetooth -> Bluetooth Host -> Bluedroid (Select)
    • Component config -> Bluetooth -> BLE Only (Enable)
    • Component config -> Bluetooth -> GATT -> Enable GATT server (should be enabled by default)
    • Component config -> ESP BLE HID -> HID Device Profile Enable (Enable)
    • You might want to increase the log level for debugging initially: Component config -> Log output -> Default log verbosity to Debug or Verbose.

2. Code (main/hid_keyboard_main.c):

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_hidd_prf_api.h" // Main HID API
#include "esp_bt_defs.h"

#define HID_KEYBOARD_TAG "HID_KEYBOARD"

// HID specific defines
#define HIDD_DEVICE_NAME            "ESP32 HID Keyboard"
#define HID_MANUFACTURER_NAME       "Espressif"
#define HID_PnP_ID_VID              0x02 // Vendor ID Source: USB Implementer's Forum
#define HID_PnP_ID_PID              0x1234 // Product ID
#define HID_PnP_ID_Version          0x0100 // Product Version

// Report Map for a standard keyboard (simplified for this example)
// This report map defines:
// - 1 byte for modifier keys (Ctrl, Shift, Alt, GUI)
// - 1 reserved byte
// - 6 bytes for up to 6 simultaneous key presses
static const uint8_t hid_report_map[] = {
    0x05, 0x01, // Usage Page (Generic Desktop)
    0x09, 0x06, // Usage (Keyboard)
    0xA1, 0x01, // Collection (Application)
    // Modifier keys byte
    0x05, 0x07, // Usage Page (Keyboard/Keypad)
    0x19, 0xE0, // Usage Minimum (Keyboard LeftControl)
    0x29, 0xE7, // Usage Maximum (Keyboard Right GUI)
    0x15, 0x00, // Logical Minimum (0)
    0x25, 0x01, // Logical Maximum (1)
    0x75, 0x01, // Report Size (1 bit)
    0x95, 0x08, // Report Count (8 bits)
    0x81, 0x02, // Input (Data, Variable, Absolute) - Modifier keys
    // Reserved byte
    0x95, 0x01, // Report Count (1)
    0x75, 0x08, // Report Size (8)
    0x81, 0x03, // Input (Constant, Variable, Absolute) - Reserved byte
    // Keycodes (6 keys)
    0x95, 0x06, // Report Count (6)
    0x75, 0x08, // Report Size (8)
    0x15, 0x00, // Logical Minimum (0)
    0x25, 0x65, // Logical Maximum (101) -  Typical key scan codes
    0x05, 0x07, // Usage Page (Keyboard/Keypad)
    0x19, 0x00, // Usage Minimum (Reserved (no event indicated))
    0x29, 0x65, // Usage Maximum (Keyboard Application)
    0x81, 0x00, // Input (Data, Array, Absolute) - Key codes
    // Output Report for LEDs (Num Lock, Caps Lock, Scroll Lock)
    0x95, 0x05, // Report Count (5) - 3 LEDs + 2 padding
    0x75, 0x01, // Report Size (1)
    0x05, 0x08, // Usage Page (LEDs)
    0x19, 0x01, // Usage Minimum (Num Lock)
    0x29, 0x05, // Usage Maximum (Kana) - We only care about first 3
    0x91, 0x02, // Output (Data, Variable, Absolute) - LED states
    // Padding for Output Report
    0x95, 0x01, // Report Count (1)
    0x75, 0x03, // Report Size (3) - Remaining 3 bits to make a byte
    0x91, 0x03, // Output (Constant, Variable, Absolute)
    0xC0        // End Collection
};

// HID Information characteristic value
static esp_hidd_app_param_t hidd_app_param = {
    .name = HIDD_DEVICE_NAME,
    .manufacturer = HID_MANUFACTURER_NAME,
    .pnp_vid = HID_PnP_ID_VID,
    .pnp_pid = HID_PnP_ID_PID,
    .pnp_version = HID_PnP_ID_Version,
};

// Advertising parameters
static esp_ble_adv_params_t hidd_adv_params = {
    .adv_int_min        = 0x20, // 32 slots * 0.625ms = 20ms
    .adv_int_max        = 0x30, // 48 slots * 0.625ms = 30ms
    .adv_type           = ADV_TYPE_IND,
    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC,
    .channel_map        = ADV_CHNL_ALL,
    .adv_filter_policy  = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

// Advertising data
// HID Appearance (Keyboard: 0x03C1)
static uint16_t hid_appearance = ESP_BLE_APPEARANCE_GENERIC_KEYBOARD;
static uint8_t adv_service_uuid128[32] = {
    // HID Service UUID (0x1812)
    // LSB ... MSB
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x12, 0x18, 0x00, 0x00,
};

static esp_ble_adv_data_t hidd_adv_data = {
    .set_scan_rsp = false,
    .include_name = true,
    .include_txpower = true,
    .min_interval = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec
    .appearance = ESP_BLE_APPEARANCE_GENERIC_KEYBOARD, // Keyboard appearance
    .manufacturer_len = 0,
    .p_manufacturer_data =  NULL,
    .service_data_len = 0,
    .p_service_data = NULL,
    .service_uuid_len = sizeof(adv_service_uuid128), // This should be 16 for 128-bit UUID
    .p_service_uuid = adv_service_uuid128, // This should point to the 16-bit UUID 0x1812
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

// Scan response data (optional)
static esp_ble_adv_data_t hidd_scan_rsp_data = {
    .set_scan_rsp = true,
    .include_name = true,
    .include_txpower = true,
    .appearance = ESP_BLE_APPEARANCE_GENERIC_KEYBOARD,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};


// HID connection status
static uint16_t hid_conn_id = 0;
static bool sec_conn = false; // Secure connection flag
#define HID_INPUT_REPORT_LEN 8 // Modifier byte + reserved byte + 6 keycodes
#define HID_OUTPUT_REPORT_LEN 1 // LED output report length

// Task to send key presses
xTaskHandle send_key_task_handle = NULL;

// Key codes for "HELLO"
// See USB HID Usage Tables (hut1_12v2.pdf) Section 10: Keyboard/Keypad Page for codes
#define KEY_H 0x0B
#define KEY_E 0x08
#define KEY_L 0x0F
#define KEY_O 0x12
#define KEY_ENTER 0x28
#define KEY_NONE 0x00 // No key pressed

// Function to send a key press
static void send_key(uint8_t key_code, uint8_t modifier) {
    if (!sec_conn) {
        ESP_LOGW(HID_KEYBOARD_TAG, "Not connected securely, cannot send key");
        return;
    }
    uint8_t buffer[HID_INPUT_REPORT_LEN];
    buffer[0] = modifier; // Modifier keys (e.g., shift, ctrl)
    buffer[1] = 0x00;     // Reserved
    buffer[2] = key_code; // First key
    buffer[3] = KEY_NONE; // Second key
    buffer[4] = KEY_NONE; // ...
    buffer[5] = KEY_NONE;
    buffer[6] = KEY_NONE;
    buffer[7] = KEY_NONE;

    esp_hidd_send_input_report(hid_conn_id, 0, HID_INPUT_REPORT_LEN, buffer); // Report ID 0 for this map
    ESP_LOGI(HID_KEYBOARD_TAG, "Sent key: 0x%02X, modifier: 0x%02X", key_code, modifier);
}

// Function to send "release all keys"
static void release_all_keys(void) {
    if (!sec_conn) return;
    uint8_t buffer[HID_INPUT_REPORT_LEN] = {0}; // All zeros: no modifiers, no keys
    esp_hidd_send_input_report(hid_conn_id, 0, HID_INPUT_REPORT_LEN, buffer);
    ESP_LOGI(HID_KEYBOARD_TAG, "Released all keys");
}

// Task to simulate typing "HELLO" then Enter
void send_hello_task(void *pvParameters) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(10000)); // Wait 10 seconds before typing

        if (sec_conn) {
            ESP_LOGI(HID_KEYBOARD_TAG, "Typing HELLO...");
            // H
            send_key(KEY_H, 0x02); // 0x02 for Shift
            vTaskDelay(pdMS_TO_TICKS(50));
            release_all_keys();
            vTaskDelay(pdMS_TO_TICKS(100));
            // E
            send_key(KEY_E, 0x02);
            vTaskDelay(pdMS_TO_TICKS(50));
            release_all_keys();
            vTaskDelay(pdMS_TO_TICKS(100));
            // L
            send_key(KEY_L, 0x02);
            vTaskDelay(pdMS_TO_TICKS(50));
            release_all_keys();
            vTaskDelay(pdMS_TO_TICKS(100));
            // L
            send_key(KEY_L, 0x02);
            vTaskDelay(pdMS_TO_TICKS(50));
            release_all_keys();
            vTaskDelay(pdMS_TO_TICKS(100));
            // O
            send_key(KEY_O, 0x02);
            vTaskDelay(pdMS_TO_TICKS(50));
            release_all_keys();
            vTaskDelay(pdMS_TO_TICKS(100));
            
            // Enter
            send_key(KEY_ENTER, 0x00);
            vTaskDelay(pdMS_TO_TICKS(50));
            release_all_keys();
            vTaskDelay(pdMS_TO_TICKS(100));

            ESP_LOGI(HID_KEYBOARD_TAG, "Finished typing HELLO");
        } else {
            ESP_LOGI(HID_KEYBOARD_TAG, "Not connected, waiting to type...");
        }
    }
}


// GAP callback
static void esp_hid_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
        esp_ble_gap_start_advertising(&hidd_adv_params);
        ESP_LOGI(HID_KEYBOARD_TAG, "Advertising data set, starting advertising");
        break;
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
        ESP_LOGI(HID_KEYBOARD_TAG, "Scan response data set");
        // You could start advertising here if you only set scan response after adv_data
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
            ESP_LOGI(HID_KEYBOARD_TAG, "Advertising started");
        } else {
            ESP_LOGE(HID_KEYBOARD_TAG, "Advertising start failed, error status = %x", param->adv_start_cmpl.status);
        }
        break;
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
        ESP_LOGI(HID_KEYBOARD_TAG, "Advertising stopped");
        break;
    case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
        ESP_LOGI(HID_KEYBOARD_TAG, "Update connection params status = %d, min_int = %d, max_int = %d,conn_int = %d,latency = %d, timeout = %d",
                 param->update_conn_params.status,
                 param->update_conn_params.min_int,
                 param->update_conn_params.max_int,
                 param->update_conn_params.conn_int,
                 param->update_conn_params.latency,
                 param->update_conn_params.timeout);
        break;
    // Security related events
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
        sec_conn = true;
        esp_bd_addr_t bd_addr;
        memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
        ESP_LOGI(HID_KEYBOARD_TAG, "Remote BD_ADDR: %08x%04x",
                 (bd_addr[0] << 24) + (bd_addr[1] << 16) + (bd_addr[2] << 8) + bd_addr[3],
                 (bd_addr[4] << 8) + bd_addr[5]);
        ESP_LOGI(HID_KEYBOARD_TAG, "Authentication complete, success: %s, addr_type: %d, key_type: 0x%x",
                 param->ble_security.auth_cmpl.success ? "Success" : "Fail",
                 param->ble_security.auth_cmpl.addr_type,
                 param->ble_security.auth_cmpl.key_type);

        if (param->ble_security.auth_cmpl.success) {
            ESP_LOGI(HID_KEYBOARD_TAG, "Secure connection established!");
        } else {
            ESP_LOGE(HID_KEYBOARD_TAG, "Authentication failed, reason: 0x%x", param->ble_security.auth_cmpl.fail_reason);
            sec_conn = false; // Ensure this is false if auth fails
        }
        break;
    case ESP_GAP_BLE_KEY_EVT: // Pairing key request
        ESP_LOGI(HID_KEYBOARD_TAG, "Key event: key type: 0x%x", param->ble_security.ble_key.key_type);
        break;
    case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Passkey request from remote
        ESP_LOGI(HID_KEYBOARD_TAG, "Passkey request, please enter passkey on remote device");
        // esp_ble_passkey_reply(param->ble_security.ble_req.bd_addr, true, 123456); // Example static passkey
        break;
    case ESP_GAP_BLE_OOB_REQ_EVT: // OOB data request
        ESP_LOGW(HID_KEYBOARD_TAG, "OOB request not supported");
        break;
    case ESP_GAP_BLE_SEC_REQ_EVT: // Security request from remote
        ESP_LOGI(HID_KEYBOARD_TAG, "Security request, accepting...");
        esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
        break;
    case ESP_GAP_BLE_NC_REQ_EVT: // Numeric Comparison request
        ESP_LOGI(HID_KEYBOARD_TAG, "Numeric Comparison request, passkey: %" PRIu32, param->ble_security.key_notif.passkey);
        ESP_LOGI(HID_KEYBOARD_TAG, "Please compare passkey on both devices and confirm (or reject).");
        esp_ble_confirm_reply(param->ble_security.key_notif.bd_addr, true); // Auto-confirm for testing
        break;

    default:
        ESP_LOGV(HID_KEYBOARD_TAG, "GAP Event: %d", event);
        break;
    }
}


// HID Device Profile callback
static void esp_hidd_event_callback(esp_hidd_cb_event_t event, esp_hidd_cb_param_t *param) {
    switch (event) {
    case ESP_HIDD_EVENT_REG_FINISH: { // Application registration finish
        if (param->init_finish.state == ESP_HIDD_INIT_OK) {
            ESP_LOGI(HID_KEYBOARD_TAG, "HID Application registered successfully");
            // Set advertising data
            // Corrected service UUID for advertising
            uint8_t hid_service_uuid16[2] = {0x12, 0x18}; // LSB, MSB for 0x1812
            hidd_adv_data.service_uuid_len = 2; // Length for 16-bit UUID
            hidd_adv_data.p_service_uuid = hid_service_uuid16;

            esp_ble_gap_set_device_name(HIDD_DEVICE_NAME); // Set device name
            esp_ble_gap_config_adv_data(&hidd_adv_data);
            // esp_ble_gap_config_adv_data(&hidd_scan_rsp_data); // If using scan response
            ESP_LOGI(HID_KEYBOARD_TAG, "Advertising data configured");
        } else {
            ESP_LOGE(HID_KEYBOARD_TAG, "HID Application registration failed, error: %d", param->init_finish.state);
        }
        break;
    }
    case ESP_HIDD_EVENT_DEINIT_FINISH: // Application deinitialization finish
        ESP_LOGI(HID_KEYBOARD_TAG, "HID Application deinitialized");
        break;
    case ESP_HIDD_EVENT_BLE_CONNECT: { // Connection established
        ESP_LOGI(HID_KEYBOARD_TAG, "HID Connected, conn_id: %d", param->connect.conn_id);
        hid_conn_id = param->connect.conn_id;
        // Security is typically initiated by the host or by the device if bonding info is not present
        // For HID, it's good practice to request security if not already pairing
        // esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM);
        break;
    }
    case ESP_HIDD_EVENT_BLE_DISCONNECT: { // Disconnection
        ESP_LOGI(HID_KEYBOARD_TAG, "HID Disconnected, reason: %d", param->disconnect.reason);
        sec_conn = false;
        hid_conn_id = 0;
        esp_ble_gap_start_advertising(&hidd_adv_params); // Restart advertising
        ESP_LOGI(HID_KEYBOARD_TAG, "Restarting advertising");
        break;
    }
    case ESP_HIDD_EVENT_BLE_VENDOR_REPORT_WRITE_EVT: { // Vendor specific report received (not used in this example)
        ESP_LOGI(HID_KEYBOARD_TAG, "Vendor report received, conn_id: %d, report_id: %d, len: %d",
                 param->vendor_write.conn_id, param->vendor_write.report_id, param->vendor_write.length);
        esp_log_buffer_hex(HID_KEYBOARD_TAG, param->vendor_write.data, param->vendor_write.length);
        break;
    }
    case ESP_HIDD_EVENT_BLE_LED_REPORT_WRITE_EVT: { // Output report for LEDs
        ESP_LOGI(HID_KEYBOARD_TAG, "LED Output report received, conn_id: %d, report_id: %d, len: %d, data: %02x",
                 param->led_write.conn_id, param->led_write.report_id, param->led_write.length,
                 (param->led_write.length > 0) ? param->led_write.data[0] : 0);
        // param->led_write.data[0] contains LED states:
        // Bit 0: Num Lock
        // Bit 1: Caps Lock
        // Bit 2: Scroll Lock
        // (Other bits for Kana, Compose, etc. if defined in report map)
        if (param->led_write.length > 0) {
            uint8_t leds = param->led_write.data[0];
            if (leds & 0x01) ESP_LOGI(HID_KEYBOARD_TAG, "Num Lock ON"); else ESP_LOGI(HID_KEYBOARD_TAG, "Num Lock OFF");
            if (leds & 0x02) ESP_LOGI(HID_KEYBOARD_TAG, "Caps Lock ON"); else ESP_LOGI(HID_KEYBOARD_TAG, "Caps Lock OFF");
            if (leds & 0x04) ESP_LOGI(HID_KEYBOARD_TAG, "Scroll Lock ON"); else ESP_LOGI(HID_KEYBOARD_TAG, "Scroll Lock OFF");
        }
        break;
    }
    default:
        ESP_LOGV(HID_KEYBOARD_TAG, "HIDD Event: %d", event);
        break;
    }
}

void app_main(void) {
    esp_err_t ret;

    // Initialize NVS
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Initialize Bluetooth controller
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(HID_KEYBOARD_TAG, "Initialize controller failed: %s", esp_err_to_name(ret));
        return;
    }
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(HID_KEYBOARD_TAG, "Enable controller failed: %s", esp_err_to_name(ret));
        return;
    }

    // Initialize Bluedroid stack
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(HID_KEYBOARD_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(HID_KEYBOARD_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret));
        return;
    }

    // Register HID device profile callback
    ret = esp_hidd_profile_init();
    if (ret) {
        ESP_LOGE(HID_KEYBOARD_TAG, "HID profile init failed: %s", esp_err_to_name(ret));
        return;
    }
    esp_hidd_register_callbacks(esp_hidd_event_callback);

    // Register GAP callback
    ret = esp_ble_gap_register_callback(esp_hid_gap_event_handler);
    if (ret) {
        ESP_LOGE(HID_KEYBOARD_TAG, "GAP register callback failed: %s", esp_err_to_name(ret));
        return;
    }

    // Configure HID parameters
    esp_hidd_app_param.name = HIDD_DEVICE_NAME; // Ensure these are set before registration
    esp_hidd_app_param.manufacturer = HID_MANUFACTURER_NAME;
    esp_hidd_app_param.pnp_vid = HID_PnP_ID_VID;
    esp_hidd_app_param.pnp_pid = HID_PnP_ID_PID;
    esp_hidd_app_param.pnp_version = HID_PnP_ID_Version;

    esp_hidd_app_set_params(&hidd_app_param);
    esp_hidd_set_report_map((uint8_t*)hid_report_map, sizeof(hid_report_map)); // Cast is okay here
    
    // Security Parameters
    esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND; // Secure Connections, MITM protection, Bonding
    esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; // No Input, No Output (typical for a simple keyboard without display/buttons for pairing)
    // If you had a display and keyboard for passkey entry: ESP_IO_CAP_IO
    // If you had just a display for numeric comparison: ESP_IO_CAP_OUT
    // If you had just a button for numeric comparison confirmation: ESP_IO_CAP_IN

    esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t));
    esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));

    // The HID profile registration will trigger ESP_HIDD_EVENT_REG_FINISH,
    // where advertising will be started.
    // esp_hidd_app_register(&hidd_app_param, (uint8_t*)hid_report_map, sizeof(hid_report_map)); // Old API
    // The new API seems to be more modular, with registration and then setting params/map.
    // The profile init itself should handle the internal registration with GATTS.

    // Start the task to send key presses
    xTaskCreate(send_hello_task, "send_hello_task", 2048, NULL, 5, &send_key_task_handle);

    ESP_LOGI(HID_KEYBOARD_TAG, "HID Keyboard Example Initialized. Waiting for registration and connection.");
}

A Note on the Advertising Data for HID Service UUID:

The hidd_adv_data.p_service_uuid should point to the 16-bit UUID 0x1812 for the HID service. The example code was initially using a 128-bit representation which is also valid but less common for standard services in basic advertising packets. I’ve corrected it in the ESP_HIDD_EVENT_REG_FINISH callback to use the 16-bit UUID for advertising.

Report Map: The hid_report_map is critical. The one provided is a common structure for a keyboard supporting modifier keys and up to 6 simultaneous key presses, plus an output report for LEDs. You can find many resources online for constructing HID report descriptors, including tools from USB-IF.

Key Codes: The key codes (e.g., KEY_H) are defined by the USB HID Usage Tables. 0x04 is ‘a’ or ‘A’, 0x0B is ‘h’ or ‘H’. The modifier byte determines if Shift is pressed.

3. Build, Flash, and Monitor:

  • idf.py build
  • idf.py -p (PORT) flash monitor (Replace (PORT) with your ESP32’s serial port)

4. Observe Output & Test with Host Device:

  • Serial Monitor: You should see logs indicating Bluetooth initialization, HID profile registration, advertising start.
  • Host Device (PC/Smartphone):
    1. Enable Bluetooth on your host device.
    2. Scan for new Bluetooth devices. You should see “ESP32 HID Keyboard” (or the name you configured).
    3. Attempt to pair/connect with it.
      • Depending on your host OS and the ESP32’s I/O capabilities setting, you might be prompted for a passkey or to confirm a numeric comparison. Since we set ESP_IO_CAP_NONE, it will likely use “Just Works” if the host allows, or might fail if the host strictly requires MITM protection that cannot be satisfied by ESP_IO_CAP_NONE. For robust security, you’d implement passkey entry or numeric comparison with appropriate hardware (display/buttons). For testing, some OSes are more lenient.
      • The example code includes esp_ble_confirm_reply(param->ble_security.key_notif.bd_addr, true); to auto-confirm numeric comparison for easier testing. In a production device, you must allow the user to actually compare and confirm.
    4. Once connected and paired (securely), the serial monitor should show Secure connection established!.
    5. After about 10 seconds (as per send_hello_task), open a text editor on your host device. The ESP32 should “type” HELLO followed by an Enter key.
    6. You can also observe LED status changes on the serial monitor if your host sends LED output reports (e.g., when you press Caps Lock on your physical PC keyboard while the ESP32 HID keyboard is connected).

Tip: If your host device (especially Windows) has trouble pairing or if the keyboard doesn’t work after pairing, try removing the device from the host’s Bluetooth settings and re-pairing. Sometimes, changes in the Report Map or device capabilities require a fresh pairing.

Character / Key Key Code (Hex) HID Usage Name (Keyboard/Keypad Page) Modifier Byte Example
No Key (Release) 0x00 Reserved (no event indicated) 0x00 (No modifier)
h / H 0x0B Keyboard h and H 0x00 for ‘h’, 0x02 (Left Shift) or 0x20 (Right Shift) for ‘H’
e / E 0x08 Keyboard e and E 0x00 for ‘e’, 0x02 or 0x20 for ‘E’
l / L 0x0F Keyboard l and L 0x00 for ‘l’, 0x02 or 0x20 for ‘L’
o / O 0x12 Keyboard o and O 0x00 for ‘o’, 0x02 or 0x20 for ‘O’
Enter 0x28 Keyboard Return (ENTER) 0x00 (No modifier)
Left Control N/A (Modifier) Keyboard LeftControl 0x01 (Bit 0 set)
Left Shift N/A (Modifier) Keyboard LeftShift 0x02 (Bit 1 set)
Spacebar 0x2C Keyboard Spacebar 0x00 (No modifier)

Example 2: ESP32 as a BLE HID Mouse (Conceptual)

To create a BLE HID Mouse, the overall structure is very similar to the keyboard. The main changes would be:

  1. Report Map: You need a Report Map specific to a mouse. This typically includes:
    • Usage Page (Generic Desktop)
    • Usage (Mouse)
    • Collection (Application)
      • Collection (Pointer)
        • Usage Page (Button) – For mouse buttons
        • Usage Minimum/Maximum (Button 1 to Button 3/5)
        • Logical Minimum/Maximum (0, 1)
        • Report Count (3 or 5 for buttons)
        • Report Size (1 bit per button)
        • Input (Data, Variable, Absolute)
        • Padding bits if needed to make a full byte for buttons.
        • Usage Page (Generic Desktop) – For X, Y, Wheel
        • Usage (X), Usage (Y), Usage (Wheel)
        • Logical Minimum/Maximum (e.g., -127 to 127 for relative movement, or 0 to screen res for absolute)
        • Report Size (e.g., 8 or 16 bits per axis)
        • Report Count (3, for X, Y, Wheel)
        • Input (Data, Variable, Relative for X/Y, Absolute or Relative for Wheel)
      • End Collection
    • End Collection
    A simple relative mouse input report might be: [buttons_byte, x_movement, y_movement, wheel_movement]
  2. HID Appearance: Change hid_appearance to ESP_BLE_APPEARANCE_GENERIC_MOUSE (0x03C2).
  3. Input Reports: The function sending reports (send_key in the keyboard example) would be modified to send_mouse_report. It would take parameters like button state, X delta, Y delta, and wheel delta, format them into the mouse report structure, and send it using esp_hidd_send_input_report().
  4. Device Name: Update HIDD_DEVICE_NAME to something like “ESP32 HID Mouse”.

The rest of the BLE stack initialization, GAP/HID event handling, and security setup would remain largely the same.

Variant Notes

  • ESP32, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: All these variants have full Bluetooth LE support and can implement BLE HID services as described. The ESP-IDF esp_hidd_prf_api.h provides a consistent API across these chips for HID device functionality.
  • ESP32-S2: This variant does not have Bluetooth hardware and therefore cannot implement BLE HID services or act as a BLE device. It does have USB OTG, which can be used to implement USB HID devices (covered in a different volume/chapter focusing on USB).

No significant differences in the BLE HID software implementation itself are expected across the BLE-capable variants when using ESP-IDF v5.x, as the HID profile is a standardized layer on top of GATT.

ESP32 Variant Bluetooth LE (BLE) Support HID over GATT (HOGP) Capable? Key Notes for HID Implementation
ESP32 (Original / Classic) Yes (Dual-mode: Classic BT & BLE) Yes Full support using ESP-IDF esp_hidd_prf_api.h.
ESP32-S3 Yes (BLE 5.0) Yes Full support, similar to original ESP32. May offer more processing power or different peripheral sets.
ESP32-C3 Yes (BLE 5.0) Yes RISC-V core. Full support for BLE HID. Good for lower-cost applications.
ESP32-C6 Yes (BLE 5.0, Wi-Fi 6, 802.15.4) Yes RISC-V core. Full support for BLE HID. Adds Thread/Zigbee capability.
ESP32-H2 Yes (BLE 5.2, 802.15.4) Yes RISC-V core. Optimized for low-power IoT, full BLE HID support. Primarily Thread/Zigbee focused but with strong BLE.
ESP32-S2 No No (Cannot act as BLE HID Device) Lacks Bluetooth hardware. Can implement USB HID using its USB OTG peripheral, but not BLE HID.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect Report Map Host OS may not recognize the device correctly (e.g., “Unknown Device,” generic HID device with no specific functions). Input might not work at all, or is misinterpreted (wrong keys, erratic mouse movement, features unavailable). Validate Report Map: Carefully check syntax, usage pages/IDs, logical/physical min/max, report sizes/counts. Use HID descriptor tools (e.g., USB-IF HID Descriptor Tool, online parsers) for validation.
Start Simple: Begin with a known-working, basic Report Map and add complexity incrementally.
Byte Alignment: Ensure correct item structure and byte alignment.
Host Cache: Clear the device from the host’s Bluetooth list and re-pair after Report Map changes, as hosts cache this information.
Security and Pairing Issues Pairing fails repeatedly. Device pairs but doesn’t connect as a HID device. Connection drops shortly after establishing. Input is ignored despite successful pairing. Windows can be particularly strict. Correct Auth Requirements: For keyboards, use authenticated pairing with MITM protection (e.g., ESP_LE_AUTH_REQ_SC_MITM_BOND).
Match I/O Capabilities: Set ESP_BLE_SM_IOCAP_MODE to match device hardware (e.g., ESP_IO_CAP_IO for display & keyboard, ESP_IO_CAP_NONE if no input/output for pairing – but this may limit compatibility).
Handle Security Events: Correctly respond to GAP events like ESP_GAP_BLE_NC_REQ_EVT (Numeric Comparison), ESP_GAP_BLE_PASSKEY_REQ_EVT in your GAP callback.
Bonding: Ensure NVS is initialized for bonding. Remove existing bonds on both ESP32 and host if issues persist.
Report Data Mismatch with Report Map No input registered on the host, or input is garbled/incorrect (e.g., pressing ‘A’ sends ‘Q’, mouse jumps erratically). Verify Report Length: The length of the data buffer passed to esp_hidd_send_input_report() must exactly match the total size (in bytes) of all fields defined for that specific report in the Report Map.
Check Report ID: If using Report IDs in your Report Map, ensure the correct report_id parameter is used when sending the report. If not using Report IDs (i.e., only one report of each type), use ID 0.
Data Formatting: Ensure data bytes are in the correct order and format (e.g., little-endian/big-endian if applicable, correct bit packing for modifier keys).
Forgetting HID Service UUID in Advertising Host may not recognize the device as a HID peripheral during scanning. Host might connect but not attempt to use HID services. Some OSes might not list it under “input devices.” Include UUID: Ensure advertising data (esp_ble_adv_data_t) includes the HID Service UUID (0x1812). Set .p_service_uuid to point to the UUID and .service_uuid_len correctly.
Set Appearance: Set the correct HID appearance code (e.g., ESP_BLE_APPEARANCE_GENERIC_KEYBOARD) in advertising data.
Incorrect Handling of Input Report CCCD Host device (e.g., PC, smartphone) pairs and connects, and seems to discover services, but no input (keystrokes, mouse movements) is ever received. ESP-IDF Handles It (Mostly): The esp_hidd_prf_api.h largely manages CCCD writes internally. When a host subscribes for notifications/indications, the stack is aware.
Check Connection State: Before calling esp_hidd_send_input_report(), ensure a secure connection is active and the host is likely subscribed (e.g., use a flag like sec_conn set after successful authentication/bonding and connection).
Sufficient GATT Permissions: Ensure the report characteristic has Notify or Indicate properties enabled.
Power Issues / Brownouts Device behaves erratically, disconnects randomly, fails to initialize Bluetooth, or resets. Adequate Power Supply: Ensure the ESP32 has a stable and sufficient power supply, especially during Wi-Fi/Bluetooth transmissions which can cause current spikes.
Decoupling Capacitors: Use appropriate decoupling capacitors near the ESP32’s power pins.
Check USB Cable/Port: A faulty USB cable or a port providing insufficient current can cause issues.

Exercises

  1. Custom Key Sequence:
    • Modify the send_hello_task in the keyboard example to type your name or a different short phrase. You’ll need to find the appropriate USB HID key codes for the characters you want to send.
    • Experiment with sending modifier keys (e.g., Shift for uppercase, Ctrl for shortcuts if the host OS supports them via HID).
  2. Basic Mouse Implementation – Movement:
    • Create a new project for a BLE HID mouse.
    • Define a simple mouse Report Map (e.g., 1 byte for buttons, 1 byte for X relative movement, 1 byte for Y relative movement).
    • Implement a task that periodically sends mouse input reports to move the cursor in a small square on the host screen (e.g., +10 X, 0 Y; then 0 X, +10 Y; then -10 X, 0 Y; then 0 X, -10 Y; repeat).
    • Remember to change the device name and appearance to “mouse”.
  3. Consumer Control Keys (e.g., Volume Up/Down):
    • Modify the keyboard Report Map to include Consumer Control usages. This involves adding a new Usage Page (0x0C for Consumer Devices) and specific usages like Volume Increment (0xE9), Volume Decrement (0xEA), Mute (0xE2), Play/Pause (0xCD).
    • This will likely require a separate report or a more complex report structure with a Report ID.
    • Add a function to send these consumer control reports. For example, trigger sending a “Volume Up” report via a button press (simulated or actual if you have a button connected).

Summary

  • The HID over GATT Profile (HOGP) allows standard input devices (keyboards, mice, etc.) to operate over BLE.
  • The core HID Service UUID is 0x1812.
  • Key characteristics include HID Information, Report Map, HID Control Point, and one or more Report characteristics.
  • The Report Map is critical; it describes the device’s data reports (input, output, feature) to the host using HID Usages.
  • Security (LE Secure Connections, authenticated pairing) is essential for most HID devices, especially keyboards.
  • The ESP-IDF provides the esp_hidd_prf_api.h to simplify creating BLE HID devices.
  • Input reports are sent from the ESP32 to the host (e.g., key presses, mouse movements) typically via notifications.
  • Output reports can be received from the host (e.g., keyboard LED status).

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top