ESP32-C3 BLEHID Example Fix Disconnections When Sending Keys

by Omar Yusuf 61 views

Hey guys! Today, we're diving deep into a super interesting topic: BLEHID (Bluetooth Low Energy Human Interface Device) on the ESP32-C3. Specifically, we're going to tackle a problem where an ESP32-C3 board seems to disconnect automatically when sending keys while acting as a wireless keyboard. Sounds frustrating, right? Let's break it down and figure out a solution, and more importantly, explore a solid example program to get things working smoothly.

The Challenge: ESP32-C3 Disconnecting as a BLE Keyboard

So, the core issue is that when using the ESP32-BLE-Keyboard library, the board unexpectedly disconnects from the client when you try to send keystrokes. No error messages, no exceptions, just… disconnection. This is like trying to have a conversation and the other person just hangs up mid-sentence! It’s not very helpful for debugging. The user has explored the library's code, which is quite short, but hasn't found a clear cause, and is unsure if the BLE library functions the same way as in the past, making it difficult to pinpoint the problem.

Diving into BLE and HID

To really understand what's going on, let's quickly touch on some fundamentals. BLEHID is the magic that allows devices like keyboards, mice, and game controllers to communicate wirelessly with computers and other devices using Bluetooth Low Energy. The HID part defines how these devices send input data, like key presses or mouse movements. Think of it as the language they speak. BLE provides the low-power wireless communication channel. Getting this to work reliably involves a few key steps:

  1. Advertising: The ESP32-C3 needs to advertise its presence as a BLE device, essentially saying, "Hey, I'm a keyboard!"
  2. Connection: A client device (like your computer) connects to the ESP32-C3.
  3. Service Discovery: The client discovers the services offered by the ESP32-C3, including the HID service.
  4. Data Transfer: The ESP32-C3 sends HID reports containing the keypress data.
  5. Disconnection: Either device can initiate a disconnection, or it can happen due to errors.

Why is it Disconnecting?

Now, why might the ESP32-C3 be disconnecting? Here are a few potential culprits:

  • Connection Intervals and Latency: BLE connections have connection intervals – the time between data transmissions. If these intervals aren't configured correctly, or if the ESP32-C3 is trying to send data too frequently or infrequently, it could lead to disconnections. The central device (your computer) might decide the connection is unreliable and drop it.
  • Power Management: BLE is designed for low power, but aggressive power-saving measures can sometimes cause issues. If the ESP32-C3 is going into a low-power state too quickly, it might miss connection events and disconnect.
  • Buffer Overflows or Data Corruption: While the user mentioned no errors, there could be subtle buffer overflows or data corruption issues in the HID reports being sent. This could confuse the client and cause a disconnect.
  • Interrupt Handling: Incorrect interrupt handling within the ESP32-C3's firmware could also lead to unexpected behavior, including disconnections.
  • BLE Stack Issues: It's also possible (though less likely) that there's an underlying issue within the BLE stack itself, either in the Arduino core or the ESP-IDF.

The Solution: A Runnable BLEHID Example

Okay, enough about the problems! Let's get to the solution. The ideal solution here is a clear, runnable example that demonstrates how to reliably send keystrokes using BLEHID on the ESP32-C3. This example should:

  • Establish a Stable Connection: It should connect to a host device and maintain a stable connection.
  • Send a Single Key Press: It should demonstrate how to send a single key press to the host.
  • Handle Disconnections Gracefully: If a disconnection occurs, it should attempt to reconnect.
  • Be Well-Commented: The code should be well-commented so that it's easy to understand what's going on.

Crafting the Example Code

Let's outline a basic example structure. We'll use the Arduino framework for simplicity. Here’s a conceptual sketch (we'll get to the full code later):

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <HIDTypes.h>

// HID Report Descriptor (defines keyboard layout)
static const uint8_t _hidReportDescriptor[] = {
  // ... (HID report descriptor bytes) ...
};

// BLE Services and Characteristics UUIDs
static BLEUUID serviceUUID("1812"); // HID Service UUID
static BLEUUID hidInfoUUID("2A4A"); // HID Information Characteristic UUID
static BLEUUID reportMapUUID("2A4B"); // Report Map Characteristic UUID
static BLEUUID reportUUID("2A4D");  // Report Characteristic UUID
static BLEUUID controlPointUUID("2A4C"); // HID Control Point Characteristic

BLEServer *pServer = NULL;
BLECharacteristic *pReportCharacteristic = NULL;
bool deviceConnected = false;

// Key press data structure (HID Report)
struct InputReport {
  uint8_t modifiers;
  uint8_t keycode1;
  uint8_t keycode2;
  uint8_t keycode3;
  uint8_t keycode4;
  uint8_t keycode5;
  uint8_t keycode6;
};

// Callback class for BLE events
class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
  }
};

void setup() {
  Serial.begin(115200);
  BLEDevice::init("My ESP32-C3 Keyboard");

  // Create the BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create the HID Service
  BLEService *pService = pServer->createService(serviceUUID);

  // HID Information characteristic
  BLECharacteristic *pHidInfoCharacteristic = pService->createCharacteristic(
    hidInfoUUID,
    BLECharacteristic::PROPERTY_READ
  );

  uint8_t hidInfoData[] = {0x11, 0x01, 0x00, 0x03}; // bcdHID=1.11, bCountryCode=0, Flags=RemoteWake | NormallyConnectable
  pHidInfoCharacteristic->setValue(hidInfoData, 4);

  // Report Map characteristic
  BLECharacteristic *pReportMapCharacteristic = pService->createCharacteristic(
    reportMapUUID,
    BLECharacteristic::PROPERTY_READ
  );

  pReportMapCharacteristic->setValue((uint8_t*)_hidReportDescriptor, sizeof(_hidReportDescriptor));

  // Report characteristic (for sending key presses)
  pReportCharacteristic = pService->createCharacteristic(
    reportUUID,
    BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_WRITE  // Add write property for boot mode support
  );

  pReportCharacteristic->addDescriptor(new BLE2902());

  // HID Control Point characteristic
  BLECharacteristic *pControlPointCharacteristic = pService->createCharacteristic(
    controlPointUUID,
    BLECharacteristic::PROPERTY_WRITE_NR // No Response
  );

  // Start the service
  pService->start();

  // Start advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(serviceUUID);
  pAdvertising->setScanResponse(false);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMaxPreferred(0x12);
  BLEDevice::startAdvertising();
  Serial.println("Waiting for a client connection to notify...");
}

void loop() {
  if (deviceConnected) {
    // Send a key press (e.g., 'a')
    InputReport report;
    memset(&report, 0, sizeof(report));
    report.keycode1 = 0x04; // Keycode for 'a'

    pReportCharacteristic->setValue((uint8_t*)&report, sizeof(report));
    pReportCharacteristic->notify();
    Serial.println("Key 'a' sent");

    delay(1000); // Wait a second

    // Send a key release (all keycodes set to 0)
    memset(&report, 0, sizeof(report));
    pReportCharacteristic->setValue((uint8_t*)&report, sizeof(report));
    pReportCharacteristic->notify();
    Serial.println("Key release sent");

    delay(1000); // Wait a second
  }
  delay(500);
}

Walking Through the Code

Let's break down what's happening in this example:

  • Includes: We include the necessary libraries for BLE functionality.
  • HID Report Descriptor: This is a crucial part! It defines the structure of the data we'll send to the host, telling it how to interpret key presses, mouse movements, etc. The example shows a placeholder; you'll need a proper HID report descriptor for a keyboard. You can find examples online or use tools to generate one. A HID report descriptor tells the host device what kind of data to expect from the HID device. For a keyboard, it defines the structure of the data that represents key presses, modifiers (like Shift or Ctrl), and other keyboard-related information. It's essentially a blueprint for the data being sent.
  • UUIDs: These are unique identifiers for the BLE services and characteristics we'll use. UUIDs are 128-bit values used to uniquely identify services, characteristics, and descriptors in the BLE world. They act like addresses, allowing devices to find and interact with specific functionalities offered by a BLE device. For HID devices, there are standard UUIDs defined by the Bluetooth SIG (Special Interest Group) for services like the HID service (0x1812) and characteristics like the Report characteristic (0x2A4D). Using these standard UUIDs ensures interoperability between different devices.
  • InputReport Structure: This struct defines the format of the data we'll send for a key press. It contains fields for modifiers (like Shift or Ctrl) and up to six keycodes. This struct is a critical part of the HID communication. It defines the structure of the data that the ESP32-C3 will send to the host device to represent keyboard input. The modifiers field indicates which modifier keys are pressed (e.g., Shift, Ctrl, Alt), while the keycodes fields represent the actual keys being pressed. The number of keycode fields (in this case, six) determines how many keys can be pressed simultaneously.
  • MyServerCallbacks: This class handles BLE connection and disconnection events. BLE events, such as connection and disconnection, are crucial for managing the communication link between the ESP32-C3 and the host device. The MyServerCallbacks class is designed to handle these events. The onConnect function is called when a device connects to the ESP32-C3, and the onDisconnect function is called when a device disconnects. These callbacks allow the ESP32-C3 to take appropriate actions, such as setting a flag to indicate the connection status or initiating a reconnection attempt.
  • setup(): This function initializes the BLE device, creates the services and characteristics, and starts advertising. This is the setup phase where the ESP32-C3 configures itself as a BLE HID device. It initializes the BLE stack, creates the necessary services and characteristics (like the HID service and the Report characteristic), and starts advertising. Advertising is the process of broadcasting the ESP32-C3's presence so that other devices can discover and connect to it. The setup function also sets up the HID report descriptor, which defines the format of the keyboard input data.
  • loop(): This function sends a key press ('a') and then a key release repeatedly. The loop function is the heart of the program. It's responsible for sending keyboard input to the host device. In this example, it sends a key press ('a') followed by a key release. This is achieved by setting the appropriate keycode in the InputReport structure and then sending the report through the Report characteristic. The delays are added to ensure that the key press and release are registered correctly by the host device.

Key Areas to Focus On

To make this example truly robust and address the original disconnection issue, you might need to adjust these areas:

  • HID Report Descriptor: Make sure this accurately reflects the keyboard layout you want to emulate. Incorrect descriptors can cause problems. Getting the HID report descriptor right is crucial for the keyboard to function correctly. It tells the host device how to interpret the data being sent. A poorly configured descriptor can lead to incorrect key mappings or other issues. You may need to adjust it depending on the specific keyboard layout you want to emulate (e.g., US, UK, German).
  • Connection Parameters: Experiment with different connection intervals and latency settings. You can adjust these parameters in the advertising setup. BLE connection parameters, such as the connection interval and latency, can significantly impact the reliability of the connection. Experimenting with different settings may be necessary to find the optimal balance between responsiveness and power consumption. Shorter connection intervals result in lower latency but consume more power, while longer intervals consume less power but may introduce delays.
  • Error Handling: Add more robust error handling to detect and respond to disconnections. Proper error handling is essential for a robust BLE application. The code should be able to detect disconnections and attempt to reconnect. You can also add logging to help diagnose issues.

Alternatives Considered

The user mentioned they aren't very familiar with the BLE stack. That's totally okay! BLE can seem complex at first. But breaking it down into smaller parts, like we've done here, can make it much more manageable. There aren't really viable alternatives to using BLE for a wireless keyboard project unless you switch to a different wireless technology altogether (like 2.4GHz). BLE is the standard for low-power wireless HID devices. If you are not familiar with BLE Stack, focusing on learning BLE concepts and practicing with examples is the best approach.

The Full(er) Code Example

Okay, let's expand on the conceptual sketch with a more complete example. Keep in mind this is still a starting point, and you might need to tweak it based on your specific needs and testing. Also, you will need to fill in the _hidReportDescriptor with a valid descriptor for a keyboard.

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <HIDTypes.h>

// **IMPORTANT: Replace with your actual HID report descriptor!**
static const uint8_t _hidReportDescriptor[] = {
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x06,                    // USAGE (Keyboard)
    0xa1, 0x01,                    // COLLECTION (Application)
    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)
    0x95, 0x08,                    //   REPORT_COUNT (8)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x81, 0x01,                    //   INPUT (Cnst,Var,Abs)
    0x95, 0x05,                    //   REPORT_COUNT (5)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x05, 0x08,                    //   USAGE_PAGE (LEDs)
    0x19, 0x01,                    //   USAGE_MINIMUM (Num Lock)
    0x29, 0x05,                    //   USAGE_MAXIMUM (Kana)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x75, 0x03,                    //   REPORT_SIZE (3)
    0x91, 0x01,                    //   OUTPUT (Cnst,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    0x95, 0x06,                    //   REPORT_COUNT (6)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x65,                    //   LOGICAL_MAXIMUM (101)
    0x05, 0x07,                    //   USAGE_PAGE (Keyboard/Keypad)
    0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
    0xc0                           // END_COLLECTION
};  

static BLEUUID serviceUUID("1812"); // HID Service UUID
static BLEUUID hidInfoUUID("2A4A"); // HID Information Characteristic UUID
static BLEUUID reportMapUUID("2A4B"); // Report Map Characteristic UUID
static BLEUUID reportUUID("2A4D");  // Report Characteristic UUID
static BLEUUID controlPointUUID("2A4C"); // HID Control Point Characteristic

BLEServer *pServer = NULL;
BLECharacteristic *pReportCharacteristic = NULL;
bool deviceConnected = false;
int disconnectCount = 0; // Counter for disconnections
const int maxDisconnects = 3; // Maximum disconnections before reset

struct InputReport {
  uint8_t modifiers;
  uint8_t keycode1;
  uint8_t keycode2;
  uint8_t keycode3;
  uint8_t keycode4;
  uint8_t keycode5;
  uint8_t keycode6;
};

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("Client Connected!");
  };

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    disconnectCount++;
    Serial.println("Client Disconnected!");
    Serial.print("Disconnect Count: ");
    Serial.println(disconnectCount);

    if (disconnectCount >= maxDisconnects) {
      Serial.println("Maximum disconnections reached. Resetting...");
      ESP.restart(); // Reset the ESP32
    }
    // Optionally, add a delay and try to start advertising again
    // delay(2000); // Wait 2 seconds
    // pServer->startAdvertising(); // Start advertising again
  }
};

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLEHID Keyboard");

  BLEDevice::init("My ESP32-C3 Keyboard");

  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  BLEService *pService = pServer->createService(serviceUUID);

  BLECharacteristic *pHidInfoCharacteristic = pService->createCharacteristic(
    hidInfoUUID,
    BLECharacteristic::PROPERTY_READ
  );

  uint8_t hidInfoData[] = {0x11, 0x01, 0x00, 0x03}; // bcdHID=1.11, bCountryCode=0, Flags=RemoteWake | NormallyConnectable
  pHidInfoCharacteristic->setValue(hidInfoData, 4);

  BLECharacteristic *pReportMapCharacteristic = pService->createCharacteristic(
    reportMapUUID,
    BLECharacteristic::PROPERTY_READ
  );

  pReportMapCharacteristic->setValue((uint8_t*)_hidReportDescriptor, sizeof(_hidReportDescriptor));

  pReportCharacteristic = pService->createCharacteristic(
    reportUUID,
    BLECharacteristic::PROPERTY_NOTIFY |
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_WRITE // Add write property for boot mode support
  );
  pReportCharacteristic->addDescriptor(new BLE2902());

  BLECharacteristic *pControlPointCharacteristic = pService->createCharacteristic(
    controlPointUUID,
    BLECharacteristic::PROPERTY_WRITE_NR // No Response
  );

  pService->start();

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(serviceUUID);
  pAdvertising->setScanResponse(false);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMaxPreferred(0x12);
  BLEDevice::startAdvertising();
  Serial.println("Waiting for a client connection to notify...");
}

void loop() {
  if (deviceConnected) {
    Serial.println("Sending key 'a'...");
    InputReport report;
    memset(&report, 0, sizeof(report));
    report.keycode1 = 0x04; // Keycode for 'a'

    pReportCharacteristic->setValue((uint8_t*)&report, sizeof(report));
    pReportCharacteristic->notify();
    Serial.println("Key 'a' sent");

    delay(1000); // Wait a second

    Serial.println("Releasing key...");
    memset(&report, 0, sizeof(report));
    pReportCharacteristic->setValue((uint8_t*)&report, sizeof(report));
    pReportCharacteristic->notify();
    Serial.println("Key released");

    delay(1000); // Wait a second
  } else {
    Serial.print("."); // Indicate we're still waiting
    delay(5000); // Check less frequently when disconnected
  }

  delay(500);
}

Key Improvements in This Version

  • More Complete HID Report Descriptor: I've included a basic HID report descriptor for a keyboard. You should verify and potentially adjust this to match your specific keyboard requirements. This example assumes a standard keyboard layout. You can use tools like the HID Descriptor Tool (search online) to help generate a more specific descriptor if needed.
  • Disconnect Counter and Reset: This version includes a disconnectCount and maxDisconnects. If the device disconnects too many times, it will reset the ESP32. This can help recover from persistent connection issues. It is a simple way to handle repeated disconnections. If the ESP32 disconnects from the host device multiple times, it may indicate a more serious problem. Resetting the ESP32 can sometimes resolve these issues by restarting the BLE stack and other peripherals.
  • Clearer Serial Output: Added more Serial.println() statements to provide better feedback on what's happening. More verbose serial output makes it easier to debug issues. You can see when the device connects, disconnects, sends key presses, and so on. This helps you track the program's execution and identify potential problems.
  • Handling Disconnections (More Robustly): The onDisconnect callback now increments a disconnect counter. If the counter reaches a threshold, the ESP32 will restart. This is a basic form of fault tolerance. Restarting the ESP32 is a drastic measure, but it can be effective in situations where the BLE stack gets into a bad state. You could potentially add more sophisticated reconnection logic here, but for a simple example, this is a reasonable approach. Instead of just restarting, you could try to re-initialize the BLE stack or re-start advertising.
  • else in loop(): Added an else to the loop() function so it prints a . to the serial monitor every 5 seconds when it's not connected. This gives you visual feedback that the ESP32 is still running and trying to connect. It is helpful to have some visual indication that the program is still running even when not connected. This avoids confusion and helps you distinguish between the program being stuck and the program simply waiting for a connection.

Final Thoughts and Next Steps

This example should give you a solid foundation for building a BLEHID keyboard with the ESP32-C3. Remember, the key to solving the original disconnection issue likely lies in carefully configuring the HID report descriptor, experimenting with connection parameters, and implementing robust error handling.

Next Steps for the User:

  • Test the Example: Load this code onto your ESP32-C3 and see if it works! Open your serial monitor to see the output.
  • Verify HID Report Descriptor: Double-check that the HID report descriptor is correct for your needs. Use a HID descriptor tool to analyze and modify it if necessary.
  • Experiment with Connection Parameters: Try adjusting the setMinPreferred() and setMaxPreferred() values in the advertising setup. You could also explore other BLE connection parameters if needed.
  • Add More Key Presses: Extend the loop() function to send different key presses and combinations.
  • Implement a More Sophisticated Keyboard: Think about adding features like modifier keys (Shift, Ctrl), multiple key presses, and custom key mappings.
  • Explore BLE Sniffing: If you're still having trouble, consider using a BLE sniffer to capture the communication between the ESP32-C3 and the host device. This can give you valuable insights into what's going wrong. BLE sniffers are tools that capture the raw BLE packets being exchanged between devices. They can be invaluable for debugging connection issues and understanding the BLE protocol in detail.

By working through this example and focusing on the key areas, you'll be well on your way to building a reliable BLEHID keyboard with your ESP32-C3. Good luck, and happy coding!