Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,18 @@ Custom [esphome](https://esphome.io/) component to implement a virtual BLE keybo

```yaml
external_components:
- source: github://dmamontov/esphome-blekeyboard
- source: github://zachdekoning/esphome-blekeyboard
```

### Enable BT support (required for ESPHome 2025.10+)
As of ESPHome 2025.10+ onwards, you must manually enable 'CONFIG_BT_ENABLED' in the framework sdkconfig_options

```yaml
framework:
type: arduino
version: latest
sdkconfig_options:
CONFIG_BT_ENABLED: "y"
```

### Configuration
Expand All @@ -64,17 +75,62 @@ ble_keyboard:
manufacturer_id: "MamonTech"
battery_level: 50
reconnect: true
advertise_on_start: true
buttons: true
use_default_libs: false
pairing_code: 123456
```

* **id** (Optional, string): Component ID. Needed for action;
* **name** (Optional, string): Keyboard name (default: Esp32BleKeyboard);
* **manufacturer_id** (Optional, string): Keyboard manufacturer (default: Esp32BleKeyboard);
* **battery_level** (Optional, int): Keyboard battery level (default: 100);
* **reconnect** (Optional, bool): Automatic reconnect service after disconnecting the device. (default: true);
* **advertise_on_start** (Optional, bool): Automatic advertisement when the ESP device starts. (default: true);
* **buttons** (Optional, bool): Whether to add separate buttons for [keys](https://github.com/dmamontov/esphome-blekeyboard/wiki/Keys) (default: true);
* **use_default_libs** (Optional, bool): Whether to use the arduino standard library. (default: false).
* **pairing_code** (Optional, int): The pairing code required to be entered on the connecting device. (default: 123456).

#### Controlling keyboard availability when `advertise_on_start` is `False`

By default, the keyboard advertises its existence on start. You can turn
this off, but this creates a problem — how to ensure that the keyboard
is available on demand?

In your ESP description, you can create a switch that does exactly that:

```
globals:
- id: keyboard_enabled
type: bool
restore_value: no
initial_value: "false"

switch:
- platform: template
name: Enabled
icon: mdi:power
lambda: |-
return id(keyboard_enabled);
return true;
} else {
return false;
}
turn_on_action:
- ble_keyboard.start: le_keyboard
- globals.set:
id: keyboard_enabled
value: "true"
turn_off_action:
- ble_keyboard.stop: le_keyboard
- globals.set:
id: keyboard_enabled
value: "false"
```

When toggled on, the switch will start the keyboard and begin advertising
it. When toggled off, the keyboard will disconnect all clients and stop
advertising — ensuring clients cannot reconnect.

### Actions

Expand Down Expand Up @@ -157,4 +213,4 @@ ble_keyboard.stop: my_ble_keyboard
## Credits
* Thanks to all [ESPHome](https://github.com/esphome/esphome) contributors;
* Thanks to [@T-vK](https://github.com/T-vK) for the [ESP32-BLE-Keyboard](https://github.com/T-vK/ESP32-BLE-Keyboard) library;
* Thanks to [@h2zero](https://github.com/h2zero) for the [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library.
* Thanks to [@h2zero](https://github.com/h2zero) for the [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library.
13 changes: 13 additions & 0 deletions components/ble_keyboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
CONF_BUTTONS,
CONF_KEYS,
CONF_RECONNECT,
CONF_ADVERTISE_ON_START,
CONF_TEXT,
CONF_USE_DEFAULT_LIBS,
CONF_PAIRING_CODE,
DOMAIN,
LIBS_ADDITIONAL,
LIBS_DEFAULT,
Expand All @@ -66,8 +68,10 @@
cv.Optional(CONF_MANUFACTURER_ID, default=COMPONENT_CLASS): cv.Length(min=1),
cv.Optional(CONF_BATTERY_LEVEL, default=100): cv.int_range(min=0, max=100),
cv.Optional(CONF_RECONNECT, default=True): cv.boolean,
cv.Optional(CONF_ADVERTISE_ON_START, default=True): cv.boolean,
cv.Optional(CONF_USE_DEFAULT_LIBS, default=False): cv.boolean,
cv.Optional(CONF_BUTTONS, default=True): cv.boolean,
cv.Optional(CONF_PAIRING_CODE, default=123456): cv.int_range(min=0, max=999999),
}
)

Expand All @@ -90,6 +94,8 @@ async def to_code(config: dict) -> None:
config[CONF_MANUFACTURER_ID],
config[CONF_BATTERY_LEVEL],
config[CONF_RECONNECT],
config[CONF_ADVERTISE_ON_START],
config[CONF_PAIRING_CODE],
)

await cg.register_component(var, config)
Expand Down Expand Up @@ -171,6 +177,13 @@ def adding_dependencies(use_default_libs: bool = True) -> None:
for lib in LIBS_ADDITIONAL: # type: ignore
cg.add_library(*lib)

# Add updated ESP32 keyboard lib
cg.add_library(
name="ESP32 BLE Keyboard",
repository="https://github.com/zachdekoning/ESP32-BLE-Keyboard",
version=None,
)

cg.add_build_flag(BUILD_FLAGS)


Expand Down
62 changes: 57 additions & 5 deletions components/ble_keyboard/ble_keyboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,49 @@ namespace ble_keyboard {
static const char *const TAG = "ble_keyboard";

void Esp32BleKeyboard::setup() {
ESP_LOGI(TAG, "Setting up...");
if (this->setup_) {
ESP_LOGW(TAG, "BLE keyboard already setup.");
return;
}

ESP_LOGCONFIG(TAG, "Setting up BLE keyboard...");

bleKeyboard.begin();

// Set security settings for bonding so phones/tablets will properly reconnect on device reboot
NimBLEDevice::setSecurityAuth(true, true, true);
NimBLEDevice::setSecurityPasskey(this->pairing_code_);
// Indicate to peer device that there is no way to confirm a pairing code.
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
NimBLEDevice::setSecurityInitKey(3);

// Set BLE Tx power to max to prevent disconnects
NimBLEDevice::setPower(ESP_PWR_LVL_P9, NimBLETxPowerType::All);

pServer = BLEDevice::getServer();

ESP_LOGCONFIG(TAG, "advertiseOnDisconnect(%s)", this->reconnect_ ? "true" : "false");
pServer->advertiseOnDisconnect(this->reconnect_);

if (!this->advertise_on_start_) {
ESP_LOGCONFIG(TAG, "stopAdvertising() because advertise_on_start is false");
pServer->stopAdvertising();
}

bleKeyboard.releaseAll();
this->setup_ = true;
ESP_LOGCONFIG(TAG, "Finished setting up up BLE keyboard.");
}

void Esp32BleKeyboard::stop() {
if (!this->setup_) {
ESP_LOGW(TAG, "Attempting to use without setup. Not doing anything.");
return;
}

ESP_LOGD(TAG, "stop()");
if (this->reconnect_) {
ESP_LOGD(TAG, "advertiseOnDisconnect(false)");
pServer->advertiseOnDisconnect(false);
}

Expand All @@ -43,7 +73,13 @@ void Esp32BleKeyboard::stop() {
}

void Esp32BleKeyboard::start() {
if (!this->setup_) {
ESP_LOGW(TAG, "Attempting to use without setup. Not doing anything.");
return;
}
ESP_LOGD(TAG, "start()");
if (this->reconnect_) {
ESP_LOGD(TAG, "advertiseOnDisconnect(true)");
pServer->advertiseOnDisconnect(true);
}

Expand All @@ -63,11 +99,15 @@ bool Esp32BleKeyboard::is_connected() {
}

void Esp32BleKeyboard::update_timer() {
this->cancel_timeout((const std::string) TAG);
this->set_timeout((const std::string) TAG, release_delay_, [this]() { this->release(); });
this->cancel_timeout(TAG);
this->set_timeout(TAG, release_delay_, [this]() { this->release(); });
}

void Esp32BleKeyboard::press(std::string message) {
if (!this->setup_) {
ESP_LOGW(TAG, "Attempting to use without setup. Not doing anything.");
return;
}
if (this->is_connected()) {
if (message.length() >= 5) {
for (unsigned i = 0; i < message.length(); i += 5) {
Expand All @@ -84,6 +124,10 @@ void Esp32BleKeyboard::press(std::string message) {
}

void Esp32BleKeyboard::press(uint8_t key, bool with_timer) {
if (!this->setup_) {
ESP_LOGW(TAG, "Attempting to use without setup. Not doing anything.");
return;
}
if (this->is_connected()) {
if (with_timer) {
this->update_timer();
Expand All @@ -94,6 +138,10 @@ void Esp32BleKeyboard::press(uint8_t key, bool with_timer) {
}

void Esp32BleKeyboard::press(MediaKeyReport key, bool with_timer) {
if (!this->setup_) {
ESP_LOGW(TAG, "Attempting to use without setup. Not doing anything.");
return;
}
if (this->is_connected()) {
if (with_timer) {
this->update_timer();
Expand All @@ -103,12 +151,16 @@ void Esp32BleKeyboard::press(MediaKeyReport key, bool with_timer) {
}

void Esp32BleKeyboard::release() {
if (!this->setup_) {
ESP_LOGW(TAG, "Attempting to use without setup. Not doing anything.");
return;
}
if (this->is_connected()) {
this->cancel_timeout((const std::string) TAG);
this->cancel_timeout(TAG);
bleKeyboard.releaseAll();
}
}
} // namespace ble_keyboard
} // namespace esphome

#endif
#endif
8 changes: 7 additions & 1 deletion components/ble_keyboard/ble_keyboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ namespace esphome {
namespace ble_keyboard {
class Esp32BleKeyboard : public PollingComponent {
public:
Esp32BleKeyboard(std::string name, std::string manufacturer_id, uint8_t battery_level, bool reconnect)
Esp32BleKeyboard(std::string name, std::string manufacturer_id, uint8_t battery_level, bool reconnect,
bool advertise_on_start, uint32_t pairing_code)
: PollingComponent(1000), bleKeyboard(name, manufacturer_id, battery_level) {
reconnect_ = reconnect;
advertise_on_start_ = advertise_on_start;
pairing_code_ = pairing_code;
}

void setup() override;
Expand Down Expand Up @@ -46,7 +49,10 @@ class Esp32BleKeyboard : public PollingComponent {
BLEServer *pServer;
BleKeyboard bleKeyboard;

bool setup_{false};
bool reconnect_{true};
bool advertise_on_start_{true};
uint32_t pairing_code_{123456};
uint32_t default_delay_{100};
uint32_t release_delay_{8};
};
Expand Down
11 changes: 4 additions & 7 deletions components/ble_keyboard/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
CONF_TEXT: Final = "text"
CONF_KEYS: Final = "keys"
CONF_RECONNECT: Final = "reconnect"
CONF_ADVERTISE_ON_START: Final = "advertise_on_start"
CONF_BUTTONS: Final = "buttons"
CONF_USE_DEFAULT_LIBS: Final = "use_default_libs"
CONF_PAIRING_CODE: Final = "pairing_code"

COMPONENT_CLASS: Final = "Esp32BleKeyboard"
COMPONENT_NUMBER_CLASS: Final = "Esp32BleKeyboardNumber"
Expand All @@ -59,14 +61,9 @@
LIBS_ADDITIONAL: Final = [
(
"h2zero/NimBLE-Arduino",
"1.4.0",
"2.3.0",
None,
),
(
"t-vk/ESP32 BLE Keyboard",
"0.3.2",
None,
),
)
]

BUILD_FLAGS: Final = "-D USE_NIMBLE"
Expand Down