From 5c44cd8962e1b2329fbdecf2ce38faa401a4ec3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jul 2025 09:50:11 -1000 Subject: [PATCH 1/2] [esp32_ble] Add PHY configuration and default to 1M for compatibility --- esphome/components/esp32_ble/__init__.py | 37 +++++++++++++++++ esphome/components/esp32_ble/ble.cpp | 52 +++++++++++++++++++++++- esphome/components/esp32_ble/ble.h | 8 ++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 93bb643596..224b30aa3f 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -117,9 +117,22 @@ CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" +CONF_PREFERRED_PHY = "preferred_phy" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] +# ESP32 variants that support BLE +BLE_VARIANTS = { + const.VARIANT_ESP32, + const.VARIANT_ESP32C3, + const.VARIANT_ESP32S3, + const.VARIANT_ESP32C6, + const.VARIANT_ESP32H2, +} + +# ESP32 variants that support 2M PHY +BLE_2M_PHY_VARIANTS = BLE_VARIANTS - {const.VARIANT_ESP32} + esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) @@ -140,6 +153,13 @@ IO_CAPABILITY = { "display_yes_no": IoCapability.IO_CAP_IO, } +BLEPhy = esp32_ble_ns.enum("BLEPhy") +BLE_PHY_OPTIONS = { + "1m": BLEPhy.BLE_PHY_1M, + "2m": BLEPhy.BLE_PHY_2M, + "auto": BLEPhy.BLE_PHY_AUTO, +} + esp_power_level_t = cg.global_ns.enum("esp_power_level_t") TX_POWER_LEVELS = { @@ -153,6 +173,18 @@ TX_POWER_LEVELS = { 9: esp_power_level_t.ESP_PWR_LVL_P9, } + +def validate_phy(value: str) -> str: + """Validate PHY selection based on ESP32 variant.""" + variant = get_esp32_variant() + if value == "2m" and variant not in BLE_2M_PHY_VARIANTS: + raise cv.Invalid( + f"2M PHY is not supported on {variant}. " + f"Only supported on: {', '.join(sorted(BLE_2M_PHY_VARIANTS))}" + ) + return value + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLE), @@ -167,6 +199,10 @@ CONFIG_SCHEMA = cv.Schema( cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), + cv.Optional(CONF_PREFERRED_PHY, default="1m"): cv.All( + cv.enum(BLE_PHY_OPTIONS, lower=True), + validate_phy, + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -237,6 +273,7 @@ async def to_code(config): cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) + cg.add(var.set_preferred_phy(config[CONF_PREFERRED_PHY])) if (name := config.get(CONF_NAME)) is not None: cg.add(var.set_name(name)) await cg.register_component(var, config) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 6b4ce07f15..97452c508a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -23,6 +23,35 @@ namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; +static const char *phy_mode_to_string(BLEPhy phy) { + switch (phy) { + case BLE_PHY_1M: + return "1M"; + case BLE_PHY_2M: + return "2M"; + case BLE_PHY_AUTO: + return "AUTO"; + default: + return "UNKNOWN"; + } +} + +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32H2) +static uint8_t phy_mode_to_mask(BLEPhy phy) { + switch (phy) { + case BLE_PHY_1M: + return ESP_BLE_GAP_PHY_1M_PREF_MASK; + case BLE_PHY_2M: + return ESP_BLE_GAP_PHY_2M_PREF_MASK; + case BLE_PHY_AUTO: + return ESP_BLE_GAP_PHY_1M_PREF_MASK | ESP_BLE_GAP_PHY_2M_PREF_MASK; + default: + return ESP_BLE_GAP_PHY_1M_PREF_MASK; // Default to 1M + } +} +#endif + void ESP32BLE::setup() { global_ble = this; if (!ble_pre_setup_()) { @@ -208,6 +237,23 @@ bool ESP32BLE::ble_setup_() { return false; } + // Configure PHY settings +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32H2) + // Only newer ESP32 variants support PHY configuration + if (this->preferred_phy_ != BLE_PHY_AUTO) { + uint8_t phy_mask = phy_mode_to_mask(this->preferred_phy_); + + err = esp_ble_gap_set_preferred_default_phy(phy_mask, phy_mask); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_gap_set_preferred_default_phy failed: %d", err); + // Not a fatal error, continue + } else { + ESP_LOGD(TAG, "Set preferred PHY to %s", phy_mode_to_string(this->preferred_phy_)); + } + } +#endif + // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT @@ -515,8 +561,10 @@ void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" - " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + " IO Capability: %s\n" + " Preferred PHY: %s", + format_mac_address_pretty(mac_address).c_str(), io_capability_s, + phy_mode_to_string(this->preferred_phy_)); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 543b2f26a3..abe0d57aa0 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -55,6 +55,12 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; +enum BLEPhy : uint8_t { + BLE_PHY_1M = 0x01, + BLE_PHY_2M = 0x02, + BLE_PHY_AUTO = 0x03, +}; + enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, @@ -98,6 +104,7 @@ class BLEStatusEventHandler { class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } + void set_preferred_phy(BLEPhy phy) { this->preferred_phy_ = phy; } void set_advertising_cycle_time(uint32_t advertising_cycle_time) { this->advertising_cycle_time_ = advertising_cycle_time; @@ -170,6 +177,7 @@ class ESP32BLE : public Component { // 1-byte aligned members (grouped together to minimize padding) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) bool enable_on_boot_{}; // 1 byte + BLEPhy preferred_phy_{BLE_PHY_1M}; // 1 byte (uint8_t enum) }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) From 51d2e7085438ca26ef3f9100ea18742046204dae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jul 2025 09:54:44 -1000 Subject: [PATCH 2/2] test --- tests/components/esp32_ble/common.yaml | 1 + tests/components/esp32_ble/test.esp32-s3-ard.yaml | 2 ++ tests/components/esp32_ble/test.esp32-s3-idf.yaml | 2 ++ 3 files changed, 5 insertions(+) create mode 100644 tests/components/esp32_ble/test.esp32-s3-ard.yaml create mode 100644 tests/components/esp32_ble/test.esp32-s3-idf.yaml diff --git a/tests/components/esp32_ble/common.yaml b/tests/components/esp32_ble/common.yaml index 76b35fc8f8..86d5dcee60 100644 --- a/tests/components/esp32_ble/common.yaml +++ b/tests/components/esp32_ble/common.yaml @@ -1,2 +1,3 @@ esp32_ble: io_capability: keyboard_only + # Default configuration - should use 1m PHY diff --git a/tests/components/esp32_ble/test.esp32-s3-ard.yaml b/tests/components/esp32_ble/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..3c05b93858 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-s3-ard.yaml @@ -0,0 +1,2 @@ +esp32_ble: + preferred_phy: 2m diff --git a/tests/components/esp32_ble/test.esp32-s3-idf.yaml b/tests/components/esp32_ble/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8f1d32779d --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-s3-idf.yaml @@ -0,0 +1,2 @@ +esp32_ble: + preferred_phy: auto