Skip to content

Instantly share code, notes, and snippets.

@manuelbl
Created August 3, 2019 09:12
Show Gist options
  • Save manuelbl/66f059effc8a7be148adb1f104666467 to your computer and use it in GitHub Desktop.
Save manuelbl/66f059effc8a7be148adb1f104666467 to your computer and use it in GitHub Desktop.
ESP32 as Bluetooth Keyboard

ESP32 as Bluetooth Keyboard

With its built-in Bluetooth capabilities, the ESP32 can act as a Bluetooth keyboard. The below code is a minimal example of how to achieve it. It will generate the key strokes for a message whenever a button attached to the ESP32 is pressed.

For the example setup, a momentary button should be connected to pin 2 and to ground. Pin 2 will be configured as an input with pull-up.

In order to receive the message, add the ESP32 as a Bluetooth keyboard of your computer or mobile phone:

  1. Go to your computers/phones settings
  2. Ensure Bluetooth is turned on
  3. Scan for Bluetooth devices
  4. Connect to the device called "ESP32 Keyboard"
  5. Open an empty document in a text editor
  6. Press the button attached to the ESP32

The code has been written for the Arduino framework. I recommend using PlatformIO for development as it is far superior to the Arduino IDE while still taking full advantage of the Arduino ecosystem (libraries, support etc.)

/*
* Sample program for ESP32 acting as a Bluetooth keyboard
*
* Copyright (c) 2019 Manuel Bl
*
* Licensed under MIT License
* https://opensource.org/licenses/MIT
*/
//
// This program lets an ESP32 act as a keyboard connected via Bluetooth.
// When a button attached to the ESP32 is pressed, it will generate the key strokes for a message.
//
// For the setup, a momentary button should be connected to pin 2 and to ground.
// Pin 2 will be configured as an input with pull-up.
//
// In order to receive the message, add the ESP32 as a Bluetooth keyboard of your computer
// or mobile phone:
//
// 1. Go to your computers/phones settings
// 2. Ensure Bluetooth is turned on
// 3. Scan for Bluetooth devices
// 4. Connect to the device called "ESP32 Keyboard"
// 5. Open an empty document in a text editor
// 6. Press the button attached to the ESP32
#define US_KEYBOARD 1
#include <Arduino.h>
#include "BLEDevice.h"
#include "BLEHIDDevice.h"
#include "HIDTypes.h"
#include "HIDKeyboardTypes.h"
// Change the below values if desired
#define BUTTON_PIN 2
#define MESSAGE "Hello from ESP32\n"
#define DEVICE_NAME "ESP32 Keyboard"
// Forward declarations
void bluetoothTask(void*);
void typeText(const char* text);
bool isBleConnected = false;
void setup() {
Serial.begin(115200);
// configure pin for button
pinMode(BUTTON_PIN, INPUT_PULLUP);
// start Bluetooth task
xTaskCreate(bluetoothTask, "bluetooth", 20000, NULL, 5, NULL);
}
void loop() {
if (isBleConnected && digitalRead(BUTTON_PIN) == LOW) {
// button has been pressed: type message
Serial.println(MESSAGE);
typeText(MESSAGE);
}
delay(100);
}
// Message (report) sent when a key is pressed or released
struct InputReport {
uint8_t modifiers; // bitmask: CTRL = 1, SHIFT = 2, ALT = 4
uint8_t reserved; // must be 0
uint8_t pressedKeys[6]; // up to six concurrenlty pressed keys
};
// Message (report) received when an LED's state changed
struct OutputReport {
uint8_t leds; // bitmask: num lock = 1, caps lock = 2, scroll lock = 4, compose = 8, kana = 16
};
// The report map describes the HID device (a keyboard in this case) and
// the messages (reports in HID terms) sent and received.
static const uint8_t REPORT_MAP[] = {
USAGE_PAGE(1), 0x01, // Generic Desktop Controls
USAGE(1), 0x06, // Keyboard
COLLECTION(1), 0x01, // Application
REPORT_ID(1), 0x01, // Report ID (1)
USAGE_PAGE(1), 0x07, // Keyboard/Keypad
USAGE_MINIMUM(1), 0xE0, // Keyboard Left Control
USAGE_MAXIMUM(1), 0xE7, // Keyboard Right Control
LOGICAL_MINIMUM(1), 0x00, // Each bit is either 0 or 1
LOGICAL_MAXIMUM(1), 0x01,
REPORT_COUNT(1), 0x08, // 8 bits for the modifier keys
REPORT_SIZE(1), 0x01,
HIDINPUT(1), 0x02, // Data, Var, Abs
REPORT_COUNT(1), 0x01, // 1 byte (unused)
REPORT_SIZE(1), 0x08,
HIDINPUT(1), 0x01, // Const, Array, Abs
REPORT_COUNT(1), 0x06, // 6 bytes (for up to 6 concurrently pressed keys)
REPORT_SIZE(1), 0x08,
LOGICAL_MINIMUM(1), 0x00,
LOGICAL_MAXIMUM(1), 0x65, // 101 keys
USAGE_MINIMUM(1), 0x00,
USAGE_MAXIMUM(1), 0x65,
HIDINPUT(1), 0x00, // Data, Array, Abs
REPORT_COUNT(1), 0x05, // 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
REPORT_SIZE(1), 0x01,
USAGE_PAGE(1), 0x08, // LEDs
USAGE_MINIMUM(1), 0x01, // Num Lock
USAGE_MAXIMUM(1), 0x05, // Kana
LOGICAL_MINIMUM(1), 0x00,
LOGICAL_MAXIMUM(1), 0x01,
HIDOUTPUT(1), 0x02, // Data, Var, Abs
REPORT_COUNT(1), 0x01, // 3 bits (Padding)
REPORT_SIZE(1), 0x03,
HIDOUTPUT(1), 0x01, // Const, Array, Abs
END_COLLECTION(0) // End application collection
};
BLEHIDDevice* hid;
BLECharacteristic* input;
BLECharacteristic* output;
const InputReport NO_KEY_PRESSED = { };
/*
* Callbacks related to BLE connection
*/
class BleKeyboardCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* server) {
isBleConnected = true;
// Allow notifications for characteristics
BLE2902* cccDesc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
cccDesc->setNotifications(true);
Serial.println("Client has connected");
}
void onDisconnect(BLEServer* server) {
isBleConnected = false;
// Disallow notifications for characteristics
BLE2902* cccDesc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
cccDesc->setNotifications(false);
Serial.println("Client has disconnected");
}
};
/*
* Called when the client (computer, smart phone) wants to turn on or off
* the LEDs in the keyboard.
*
* bit 0 - NUM LOCK
* bit 1 - CAPS LOCK
* bit 2 - SCROLL LOCK
*/
class OutputCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* characteristic) {
OutputReport* report = (OutputReport*) characteristic->getData();
Serial.print("LED state: ");
Serial.print((int) report->leds);
Serial.println();
}
};
void bluetoothTask(void*) {
// initialize the device
BLEDevice::init(DEVICE_NAME);
BLEServer* server = BLEDevice::createServer();
server->setCallbacks(new BleKeyboardCallbacks());
// create an HID device
hid = new BLEHIDDevice(server);
input = hid->inputReport(1); // report ID
output = hid->outputReport(1); // report ID
output->setCallbacks(new OutputCallbacks());
// set manufacturer name
hid->manufacturer()->setValue("Maker Community");
// set USB vendor and product ID
hid->pnp(0x02, 0xe502, 0xa111, 0x0210);
// information about HID device: device is not localized, device can be connected
hid->hidInfo(0x00, 0x02);
// Security: device requires bonding
BLESecurity* security = new BLESecurity();
security->setAuthenticationMode(ESP_LE_AUTH_BOND);
// set report map
hid->reportMap((uint8_t*)REPORT_MAP, sizeof(REPORT_MAP));
hid->startServices();
// set battery level to 100%
hid->setBatteryLevel(100);
// advertise the services
BLEAdvertising* advertising = server->getAdvertising();
advertising->setAppearance(HID_KEYBOARD);
advertising->addServiceUUID(hid->hidService()->getUUID());
advertising->addServiceUUID(hid->deviceInfo()->getUUID());
advertising->addServiceUUID(hid->batteryService()->getUUID());
advertising->start();
Serial.println("BLE ready");
delay(portMAX_DELAY);
};
void typeText(const char* text) {
int len = strlen(text);
for (int i = 0; i < len; i++) {
// translate character to key combination
uint8_t val = (uint8_t)text[i];
if (val > KEYMAP_SIZE)
continue; // character not available on keyboard - skip
KEYMAP map = keymap[val];
// create input report
InputReport report = {
.modifiers = map.modifier,
.reserved = 0,
.pressedKeys = {
map.usage,
0, 0, 0, 0, 0
}
};
// send the input report
input->setValue((uint8_t*)&report, sizeof(report));
input->notify();
delay(5);
// release all keys between two characters; otherwise two identical
// consecutive characters are treated as just one key press
input->setValue((uint8_t*)&NO_KEY_PRESSED, sizeof(NO_KEY_PRESSED));
input->notify();
delay(5);
}
}
@intg76
Copy link

intg76 commented May 25, 2024

Great example, but i can't understand, can this library send combo like "win+shift+KEY_RIGHT"? How to do it?

@drowe
Copy link

drowe commented Jun 4, 2024

This example works great! One bit I'd add for future folks that find this - I had the issue where if my phone/device disconnected, it would leave the ESP32 in a stuck state - unable to reconnect for anything. Found in a related issue about restarting the advertising on client disconnected - so, I added

Serial.println("Restarting advertising...");
server->getAdvertising()->start();

in the onDisconnect method (line 154) - and if my device disconnected (turned off) it would auto-reconnect because the ESP32 would start advertising again.

@OneDuckyBoy
Copy link

Is it possible to use it as media controls?(play/pause, volume up and down, previous and next song)
I tryed unsuccessfully to use

void sendKeyCode(uint8_t keyCode) {
    // create input report
    InputReport report = {
        .modifiers = 0,
        .reserved = 0,
        .pressedKeys = { keyCode, 0, 0, 0, 0, 0 }
    };

    // send the input report
    input->setValue((uint8_t*)&report, sizeof(report));
    input->notify();
    delay(5);

    // release all keys
    input->setValue((uint8_t*)&NO_KEY_PRESSED, sizeof(NO_KEY_PRESSED));
    input->notify();
    delay(5);
}

and 
sendKeyCode(playpause);
#define playpause 0xCD 

I asked chatGPT for help and it gave me

struct ConsumerReport {
    uint16_t usage;
};

const ConsumerReport NO_CONSUMER_KEY_PRESSED = { 0x0000 };

void sendConsumerKey(uint16_t usage) {
    // Create consumer control report
    ConsumerReport report = { usage };

    // Send the consumer control report
    input->setValue((uint8_t*)&report, sizeof(report));
    input->notify();
    delay(5);

    // Release all keys
    input->setValue((uint8_t*)&NO_CONSUMER_KEY_PRESSED, sizeof(NO_CONSUMER_KEY_PRESSED));
    input->notify();
    delay(5);
}

whitch didn`t work either

I may be the problem, because I don't entirely understand the code and don't know how to fix it

I would be super grateful if there is somebody who would be able to help!

@alanmiller
Copy link

alanmiller commented Jul 7, 2024

@manuelbl You got me right. Your short example is very good, because now I really understand that function more. Thank You! Maybe this is interesting: With some AI help I modified the function so that it accepts this format:
.....
This allows to read in commands with variable modifiers. Thank you for your elegant code! I would also like to publish my project on github and I would add your license. Could I do so? All the Best!

@ctc-nick
I'd like to implement the same but net well versed in cpp, could you share how you are storing your mappings in Json? ie what library are you using for that? Does you code actually load a json file? Does that file get copied to the ESP32 device or just compiled into the image?

@legitlee
Copy link

legitlee commented Jul 8, 2024 via email

@alanmiller
Copy link

Thanks @legitlee but I was asking @ctc-nick how/which json library he was using for the mapping buttons to pins.
I figured out my own solution in plain C using an array of structs. I am creating a control box for my golf simulator and have 13 buttons.

I defined the pins and keycodes in a header file

#define CLUB_UP_PIN      13 // gpio 13
#define CLUB_DOWN_PIN    12 // gpio 12
#define KEY_CLUB_UP      0x0c // letter i
#define KEY_CLUB_DOWN    0x0e // letter k

then in main I have

typedef struct {
    std::string name;
    int pin;
    uint8_t keycode;
    uint8_t modifier;
    bool state;
} Button;

Button buttons[BUTTONS] = {
    {"Club-Up",      CLUB_UP_PIN,     KEY_CLUB_UP,     KEY_NONE,  false},
    {"Club-Down",    CLUB_DOWN_PIN,   KEY_CLUB_DOWN,   KEY_NONE,  false},
    {"Fly-Over",     FLYOVER_PIN,     KEY_FLYOVER,     KEY_NONE,  false},
    {"Scorecard",    SCORECARD_PIN,   KEY_SCORECARD,   KEY_NONE,  false},
    {"Range-Finder", RANGE_FINDER_PIN,KEY_RANGE_FINDER,KEY_NONE,  false},
    {"Next-Hole",    HOLE_UP_PIN,     KEY_HOLE_UP,     KEY_NONE,  false},
    {"Previous-Hole",HOLE_DOWN_PIN,   KEY_HOLE_DOWN,   KEY_NONE,  false},
    {"Aim-Left",     AIM_LEFT_PIN,    KEY_AIM_LEFT,    KEY_NONE,  false},
    {"Aim-Right",    AIM_RIGHT_PIN,   KEY_AIM_RIGHT,   KEY_NONE,  false},
    {"Aim-Down",     AIM_DOWN_PIN,    KEY_AIM_DOWN,    KEY_NONE,  false},
    {"Aim-Up",       AIM_UP_PIN,      KEY_AIM_UP,      KEY_NONE,  false},
    {"Mulligan",     MULLIGAN_PIN,    KEY_MULLIGAN,    KEY_LCTRL, false},
    {"Aimpoint",     AIMPOINT_PIN,    KEY_AIMPOINT,    KEY_NONE,  false}
};

then in setup()

int i;
    for(i = 0; i < BUTTONS; i ++) {
        pinMode(buttons[i].pin,INPUT_PULLUP);
    }

and in loop() I call

sendKeyCode(buttons[keyIndex].keycode,buttons[keyIndex].modifier);

@ardmgtm
Copy link

ardmgtm commented Jul 22, 2024

can i control media use this, like playing/pause media, next/prev track, and volume up/down ?

@picKEYdotnet
Copy link

I am new to BLE/ESP32 and learning by trial & error.
What I'd like to do is to write a string to Windows/iOS where (as if my ESP32 device is a Bluetooth keyboard) the Windows cursor is poinint at. That can be a open Notepad or any login box.

When I compile the above code, it fails and gives the following message which, of course I do not have much understandin of them.
Currently I am using

  1. Arduino IDE 2.3.2 version
  2. Libraries offered by ESP32_BLE_Arduino v1.0,1
  3. It seems the error(s) is from BLEServer.h and/or BLEDevice that 'ringbuffer_type_t is not delarred, whatever it means???

Another concern I have is the resulting code size when it get to that point that it takes more then approx. 1.2meg. This BLE function is more of optional feature I am working on and should not replace any of the main tasks I am working on.

I appreciate in advance for any help I may get from your readers!!!

@solilu7
Copy link

solilu7 commented Aug 5, 2024

Hi,
i've a problem with some android devices with some Andorid devices (10 12 and 13 version, all chinese brand, oukitel, ulefone, hugerock)
EP32-C3 boards, i have this behavior:

I connect Esp32 board to smatphone searching a new device, it connects.
Then i switch-off and then switch-on (or simply reboot) Esp32 board, on the smatphone ESP-32 appears on "device cennected" section just for an instant, then disappears (obviusly it is not connected).
if i want reconnect i have to repeat all procedure (searching ecc).

@UsernamePlankalkul
Copy link

Hi . thanks for this Code .
is possible to send Function Keys (FN) from F13 to F24 ?

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment