diff --git a/README.md b/README.md index e7d7953..b927651 100644 --- a/README.md +++ b/README.md @@ -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 @@ -64,8 +75,10 @@ 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; @@ -73,8 +86,51 @@ ble_keyboard: * **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 @@ -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. \ No newline at end of file +* Thanks to [@h2zero](https://github.com/h2zero) for the [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) library. diff --git a/components/ble_keyboard/__init__.py b/components/ble_keyboard/__init__.py index 2e79757..b492bb5 100644 --- a/components/ble_keyboard/__init__.py +++ b/components/ble_keyboard/__init__.py @@ -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, @@ -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), } ) @@ -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) @@ -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) diff --git a/components/ble_keyboard/ble_keyboard.cpp b/components/ble_keyboard/ble_keyboard.cpp index 178d316..5bded15 100644 --- a/components/ble_keyboard/ble_keyboard.cpp +++ b/components/ble_keyboard/ble_keyboard.cpp @@ -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); } @@ -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); } @@ -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) { @@ -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(); @@ -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(); @@ -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 \ No newline at end of file +#endif diff --git a/components/ble_keyboard/ble_keyboard.h b/components/ble_keyboard/ble_keyboard.h index c575f1f..eec2312 100644 --- a/components/ble_keyboard/ble_keyboard.h +++ b/components/ble_keyboard/ble_keyboard.h @@ -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; @@ -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}; }; diff --git a/components/ble_keyboard/const.py b/components/ble_keyboard/const.py index 1f20b16..1cec802 100644 --- a/components/ble_keyboard/const.py +++ b/components/ble_keyboard/const.py @@ -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" @@ -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"