Compare commits

...
Author SHA1 Message Date
Jesse Hills b03870b551 Merge pull request #22583 from esphome/bump-2026.11.0b4
2026.11.0b4
2026-11-12 16:26:56 +13:00
Jesse Hills 4a965b8b32 Bump version to 2026.11.0b4 2026-11-12 15:42:05 +13:00
J. Nick Koston d826a19eb1 [esp32_ble] Sort GATT discovery responses by handle for ESP-IDF 5.5.6 and 6.0.3 (#22576) 2026-11-12 15:42:02 +13:00
J. Nick Koston fb54ec51c1 [gpio] Lock interrupts per slot on the one_wire bus (#22452) 2026-11-12 15:42:02 +13:00
J. Nick Koston 04b6c0d4ca [esp32_ble] Keep bond keys when a bonded device re-encrypts on ESP-IDF 5.5.6 (#22559) 2026-11-12 15:42:02 +13:00
J. Nick Koston abf282dd08 [esp32_ble_tracker] Scan at the short window while the wifi AP is running (#22566) 2026-11-12 15:41:58 +13:00
J. Nick Koston e097958ad9 [remote_transmitter] Lock interrupts only near each edge on ESP8266 (#22453) 2026-11-12 15:41:24 +13:00
jabla 8a8ad4d8da [zigbee] Rejoin only after the parent missed several polls (#22405) 2026-11-12 15:41:24 +13:00
J. Nick Koston 09b82388c9 [uart] Lock interrupts per bit in ESP8266 software serial TX at 19200 baud and below (#22450) 2026-11-12 15:41:23 +13:00
J. Nick Koston ea5407925a [wifi] Apply power save when the AP starts without networks (#22564) 2026-11-12 15:41:20 +13:00
J. Nick Koston 97732df3a5 [esp32_ble][ble_device_base][improv_ble] Send advertising as raw packets and keep Improv in the advertising packet (#22499) 2026-11-12 15:40:20 +13:00
Francesco Pace 04482ae0d4 [espectre] Update to SDK 3.2.0 (#22570) 2026-11-12 15:40:20 +13:00
Clyde Stubbs ca0dd8e292 [mipi_dsi] Keep the panel running while flash is written (#22535) 2026-11-12 15:40:20 +13:00
J. Nick Koston e75ac64cc9 [esp8266] Skip other devices' known mDNS answers without allocating (Arduino core 3.1.2-esphome.4) (#22533) 2026-11-12 15:40:20 +13:00
PolarGooseandJ. Nick Koston 212c0c1ea4 [dsmr] Update dsmr_parser to 1.11.0 (#22530)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-12 15:40:20 +13:00
Jonathan Swoboda 3e2f9bf296 [ota] Skip the auth request when the password is empty at run time (#22485) 2026-11-12 15:40:20 +13:00
J. Nick Koston b997866e86 [esp32_ble_server] Size the BLE2902 value buffer to its 2 byte value (#22454) 2026-11-12 15:40:20 +13:00
J. Nick Koston ae89db98e4 [core] Inline InterruptLock on ESP8266 (#22455) 2026-11-12 15:40:19 +13:00
Kevin Ahrendt e9bb102c1f [sendspin] Add bits_per_sample to the media source (#22313) 2026-11-12 15:40:19 +13:00
60 changed files with 1765 additions and 309 deletions

No files matched your search

+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.11.0b3
PROJECT_NUMBER = 2026.11.0b4
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+3 -3
View File
@@ -49,9 +49,9 @@ class FrameworkRelease(NamedTuple):
# Arduino core version -> its build in esphome-libs/arduino-esp8266
FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = {
Version(3, 1, 2): FrameworkRelease(
"3.1.2-esphome.3",
"3af533a85163804e164500a5b254dc23b922080e25ecb7dc37b3dd8ac84651db",
37185650,
"3.1.2-esphome.4",
"d215e439e5592ca6ecac283825d08044b2bfce9cf592e03cf33dcbcec69365e2",
37192995,
),
}
@@ -0,0 +1,161 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include "esphome/components/ble_device_base/ble_device.h"
#include "esphome/core/helpers.h"
namespace esphome::ble_device_base {
/// A 31 byte advertising or scan response payload, packed in the order and with the fit rules of Bluedroid's
/// btm_ble_build_adv_data, so the raw bytes match what the esp_ble_adv_data_t API put on air.
class AdvPayload {
public:
static constexpr size_t MAX_LEN = 31;
const uint8_t *data() const { return this->buf_.data(); }
size_t size() const { return this->len_; }
void add_flags(uint8_t flags) {
if (flags != 0 && this->left_() >= 3)
this->add_(0x01, &flags, 1);
}
void add_appearance(uint16_t appearance) {
if (appearance == 0 || this->left_() <= 3)
return;
uint8_t value[2];
put_le16(value, appearance);
this->add_(0x19, value, sizeof(value));
}
/// Complete name, or the shortened name type with what fits.
void add_name(const char *name, size_t len) {
if (this->left_() <= 2)
return;
const size_t fit = std::min(len, this->left_() - 2);
this->add_(fit < len ? 0x08 : 0x09, reinterpret_cast<const uint8_t *>(name), fit);
}
/// Truncated to what fits.
void add_manufacturer_data(std::span<const uint8_t> data) {
if (!data.empty() && this->left_() > 2)
this->add_(0xFF, data.data(), std::min(data.size(), this->left_() - 2));
}
void add_tx_power(int8_t dbm) {
if (this->left_() > 2)
this->add_(0x0A, reinterpret_cast<const uint8_t *>(&dbm), 1);
}
/// 16 and 32-bit lists (partial when they do not all fit), then the first 128-bit UUID; `skip` is left out.
void add_uuids(std::span<const ESPBTUUID> uuids, const ESPBTUUID &skip = {}) {
this->add_short_uuids_(uuids, skip, 2, 0x02);
this->add_short_uuids_(uuids, skip, 4, 0x04);
if (this->left_() < 18)
return;
for (const auto &uuid : uuids) {
if (uuid != skip && uuid_size(uuid) == 16) {
this->add_(0x07, uuid.uuid128(), 16);
return;
}
}
}
/// Raw service data element (its first bytes are the UUID); dropped whole when it does not fit.
void add_service_data(std::span<const uint8_t> data) {
if (!data.empty() && this->left_() >= data.size() + 2)
this->add_(0x16, data.data(), data.size());
}
/// Bluedroid's width for `uuid`: 2 or 4 (and its value) when it has a 16 or 32-bit form, else 16.
static size_t uuid_size(const ESPBTUUID &uuid, uint32_t *value = nullptr) {
static constexpr uint8_t BASE[12] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00};
uint32_t short_value;
if (uuid.type() == ESPBTUUID::Type::UUID16) {
short_value = uuid.uuid16();
} else if (uuid.type() == ESPBTUUID::Type::UUID32) {
short_value = uuid.uuid32();
} else {
const uint8_t *raw = uuid.uuid128();
if (memcmp(raw, BASE, sizeof(BASE)) != 0)
return 16;
short_value = encode_value_le<uint32_t>(raw + 12);
}
if (value != nullptr)
*value = short_value;
return short_value <= 0xFFFF ? 2 : 4;
}
protected:
size_t left_() const { return MAX_LEN - this->len_; }
void add_(uint8_t type, const uint8_t *value, size_t len) {
this->buf_[this->len_++] = len + 1;
this->buf_[this->len_++] = type;
memcpy(this->buf_.data() + this->len_, value, len);
this->len_ += len;
}
void add_short_uuids_(std::span<const ESPBTUUID> uuids, const ESPBTUUID &skip, size_t size, uint8_t partial_type) {
const size_t count =
std::count_if(uuids.begin(), uuids.end(), [&](const auto &u) { return u != skip && uuid_size(u) == size; });
if (count == 0 || this->left_() < 2 + size)
return;
size_t fit = std::min(count, (this->left_() - 2) / size);
this->buf_[this->len_++] = 1 + fit * size;
this->buf_[this->len_++] = fit < count ? partial_type : partial_type + 1;
for (const auto &uuid : uuids) {
if (fit == 0)
break;
uint32_t value;
if (uuid == skip || uuid_size(uuid, &value) != size)
continue;
if (size == 2) {
put_le16(this->buf_.data() + this->len_, value);
} else {
put_le32(this->buf_.data() + this->len_, value);
}
this->len_ += size;
fit--;
}
}
std::array<uint8_t, MAX_LEN> buf_{};
uint8_t len_{0};
};
/// What the services advertising carries. A set `priority_uuid` (also in `uuids`) gets the advertising packet.
struct AdvContent {
std::span<const ESPBTUUID> uuids;
ESPBTUUID priority_uuid;
std::span<const uint8_t> manufacturer_data;
std::span<const uint8_t> service_data;
const char *name;
size_t name_len;
uint16_t appearance;
int8_t tx_power_dbm;
bool name_in_adv;
};
/// Split `content` between the advertising packet and the scan response. With a priority UUID, appearance,
/// manufacturer data and the other UUIDs move to the scan response, and the service data stays only in `adv`.
inline void build_adv_packets(const AdvContent &content, AdvPayload &adv, AdvPayload &rsp) {
const bool priority = content.priority_uuid.is_set();
AdvPayload &extras = priority ? rsp : adv;
adv.add_flags(0x06); // LE general discoverable, BR/EDR not supported
extras.add_appearance(content.appearance);
if (content.name_in_adv)
adv.add_name(content.name, content.name_len);
rsp.add_name(content.name, content.name_len);
extras.add_manufacturer_data(content.manufacturer_data);
rsp.add_tx_power(content.tx_power_dbm);
if (priority) {
adv.add_uuids({&content.priority_uuid, 1});
} else {
adv.add_uuids(content.uuids);
}
rsp.add_uuids(content.uuids, content.priority_uuid);
adv.add_service_data(content.service_data);
if (!priority)
rsp.add_service_data(content.service_data);
}
} // namespace esphome::ble_device_base
+1 -1
View File
@@ -87,7 +87,7 @@ async def to_code(config: ConfigType) -> None:
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
cg.add_library("esphome/dsmr_parser", "1.10.0")
cg.add_library("esphome/dsmr_parser", "1.11.0")
def final_validate(config: ConfigType) -> None:
+51 -14
View File
@@ -39,6 +39,7 @@ from esphome.const import (
)
from esphome.core import CORE, TimePeriod
import esphome.final_validate as fv
from esphome.helpers import IS_WINDOWS
from esphome.types import ConfigType
AUTO_LOAD = ["ble_device_base"] # ble_uuid.h builds on the neutral ESPBTUUID
@@ -296,19 +297,28 @@ __all__ = [
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
# Give bluedroid_hex_wraps.cpp Bluedroid's private headers so its ESP-IDF drift
# checks compile; the bt target's include dirs are read at generate time. Only
# the native ESP-IDF toolchain generates src/CMakeLists.txt.
BLUEDROID_HEADERS_CMAKE = """\
def _wrap_bluedroid_symbols(*symbols: str) -> None:
"""Link calls to each Bluedroid symbol to its ``__wrap_`` replacement."""
for symbol in symbols:
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# libsrc.a is scanned before libbt.a, so pull the wrap in explicitly
cg.add_build_flag(f"-Wl,--undefined=__wrap_{symbol}")
def bluedroid_headers_cmake(source: str) -> str:
"""Give one esp32_ble source Bluedroid's private include dirs (native ESP-IDF toolchain only)."""
return f"""\
idf_component_get_property(esphome_bt_lib bt COMPONENT_LIB)
set_source_files_properties(
"${CMAKE_CURRENT_SOURCE_DIR}/esphome/components/esp32_ble/bluedroid_hex_wraps.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/components/esp32_ble/{source}"
PROPERTIES
INCLUDE_DIRECTORIES "$<TARGET_PROPERTY:${esphome_bt_lib},INCLUDE_DIRECTORIES>"
INCLUDE_DIRECTORIES "$<TARGET_PROPERTY:${{esphome_bt_lib}},INCLUDE_DIRECTORIES>"
COMPILE_DEFINITIONS USE_ESP32_BLE_BLUEDROID_HEADERS
)
"""
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component)
@@ -496,9 +506,7 @@ def final_validation(config: ConfigType) -> None:
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HOST_FEAT_BT", True)
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
has_ble_client = (
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
)
has_ble_client = _has_gatt_client(full_config)
# Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config
@@ -567,11 +575,26 @@ async def to_code(config: ConfigType) -> None:
and get_esp32_variant() not in ROM_SSCANF_VARIANTS
):
cg.add_define("USE_ESP32_BLE_BLUEDROID_HEX_WRAPS")
for symbol in ("btc_config_get_bin", "string_to_bdaddr"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# libsrc.a is scanned before libbt.a, so pull the wrap in explicitly
cg.add_build_flag(f"-Wl,--undefined=__wrap_{symbol}")
add_src_cmake(BLUEDROID_HEADERS_CMAKE)
_wrap_bluedroid_symbols("btc_config_get_bin", "string_to_bdaddr")
# The headers only feed bluedroid_hex_wraps.cpp's ESP-IDF drift checks
add_src_cmake(bluedroid_headers_cmake("bluedroid_hex_wraps.cpp"))
# ESP-IDF 5.5.6 drops bond keys on re-encryption (see bluedroid_bond_keys.c). Needs the
# native toolchain for Bluedroid's private headers; Windows response files lose them.
if (
CORE.using_toolchain_esp_idf
and not IS_WINDOWS
and cv.Version(5, 5, 6) <= idf_version() < cv.Version(6, 0, 0)
):
cg.add_define("USE_ESP32_BLE_BLUEDROID_BOND_KEYS")
_wrap_bluedroid_symbols("smp_check_auth_req", "btm_sec_clear_ble_keys")
add_src_cmake(bluedroid_headers_cmake("bluedroid_bond_keys.c"))
# ESP-IDF 5.5.6 and 6.0.3 end GATT discovery on a response listing handles out of order
# (see bluedroid_gattc_order.cpp). It needs no Bluedroid headers, so any toolchain works.
if _has_gatt_client(CORE.config) and _has_strict_handle_order(idf_version()):
cg.add_define("USE_ESP32_BLE_GATTC_HANDLE_ORDER")
_wrap_bluedroid_symbols("gatt_client_handle_server_rsp")
# When PSRAM and BT are used together, Bluedroid should prefer SPIRAM for
# heap allocations and use dynamic (heap-based) environment memory tables
@@ -639,11 +662,25 @@ for _name, _call in (("ble.enable", "enable()"), ("ble.disable", "disable()")):
)
def _has_gatt_client(config: ConfigType) -> bool:
"""Whether Bluedroid's GATT client is built: the tracker (and bluetooth_proxy) or esp32_ble_client."""
return "esp32_ble_tracker" in config or "esp32_ble_client" in config
def _has_strict_handle_order(version: cv.Version) -> bool:
"""Whether this ESP-IDF rejects discovery responses whose handles are out of order."""
return cv.Version(5, 5, 6) <= version < cv.Version(
6, 0, 0
) or version >= cv.Version(6, 0, 3)
# ble_advertising.cpp is fully #ifdef'd on USE_ESP32_BLE_ADVERTISING, set
# when advertising is enabled here or by esp32_ble_server / esp32_ble_beacon.
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"ble_advertising.cpp": "USE_ESP32_BLE_ADVERTISING",
"bluedroid_hex_wraps.cpp": "USE_ESP32_BLE_BLUEDROID_HEX_WRAPS",
"bluedroid_bond_keys.c": "USE_ESP32_BLE_BLUEDROID_BOND_KEYS",
"bluedroid_gattc_order.cpp": "USE_ESP32_BLE_GATTC_HANDLE_ORDER",
}
)
+45 -36
View File
@@ -195,7 +195,8 @@ void ESP32BLE::advertising_refresh() {
if (this->advertising_ref_count_ == 0) {
this->advertising_->stop();
} else {
this->advertising_->start();
this->advertising_->request_start();
App.wake_loop_threadsafe();
}
}
@@ -244,6 +245,16 @@ void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
this->advertising_refresh();
}
void ESP32BLE::advertising_set_priority_uuid(ESPBTUUID uuid) {
this->advertising_init_();
this->advertising_->set_priority_uuid(uuid);
}
void ESP32BLE::advertising_clear_priority_uuid() {
if (this->advertising_ != nullptr)
this->advertising_->clear_priority_uuid();
}
void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
this->advertising_init_();
this->advertising_->remove_service_uuid(uuid);
@@ -257,12 +268,28 @@ void ESP32BLE::advertising_init_() {
return;
this->advertising_ = new BLEAdvertising(this->advertising_cycle_time_); // NOLINT(cppcoreguidelines-owning-memory)
this->advertising_->set_scan_response(true);
this->advertising_->set_min_preferred_interval(0x06);
this->advertising_->set_appearance(this->appearance_);
}
#endif
const char *ESP32BLE::device_name(char *buffer) const {
if (this->name_ != nullptr) {
if (!App.is_name_add_mac_suffix_enabled())
return this->name_;
// Last 6 characters of the 12 character MAC address string
constexpr size_t mac_address_suffix_len = 6;
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_addr);
make_name_with_suffix_to(buffer, DEVICE_NAME_BUFFER_SIZE, this->name_, strlen(this->name_), '-',
mac_addr + mac_address_suffix_len, mac_address_suffix_len);
return buffer;
}
const auto &app_name = App.get_name();
truncate_name_to(buffer, DEVICE_NAME_BUFFER_SIZE, app_name.c_str(), app_name.length(),
App.is_name_add_mac_suffix_enabled() ? MAC_ADDRESS_SUFFIX_WITH_SEPARATOR_LEN : 0);
return buffer;
}
bool ESP32BLE::ble_setup_() {
esp_err_t err;
#ifdef CONFIG_BT_CONTROLLER_DISABLED
@@ -373,32 +400,8 @@ bool ESP32BLE::ble_setup_() {
}
#endif
// BLE device names are limited to 20 characters
// Buffer: 20 chars + null terminator
constexpr size_t ble_name_max_len = 21;
char name_buffer[ble_name_max_len];
const char *device_name;
if (this->name_ != nullptr) {
if (App.is_name_add_mac_suffix_enabled()) {
// MAC address suffix length (last 6 characters of 12-char MAC address string)
constexpr size_t mac_address_suffix_len = 6;
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_addr);
const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len;
make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr,
mac_address_suffix_len);
device_name = name_buffer;
} else {
device_name = this->name_;
}
} else {
const auto &app_name = App.get_name();
truncate_name_to(name_buffer, sizeof(name_buffer), app_name.c_str(), app_name.length(),
App.is_name_add_mac_suffix_enabled() ? MAC_ADDRESS_SUFFIX_WITH_SEPARATOR_LEN : 0);
device_name = name_buffer;
}
char name_buffer[DEVICE_NAME_BUFFER_SIZE];
const char *device_name = this->device_name(name_buffer);
err = esp_ble_gap_set_device_name(device_name);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err);
@@ -507,15 +510,11 @@ void ESP32BLE::loop() {
return;
}
#ifdef USE_ESP32_BLE_ADVERTISING
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
#endif
BLEEvent *ble_event = this->ble_events_.pop();
if (ble_event == nullptr)
if (ble_event == nullptr) {
this->advertising_loop_();
return;
}
do {
switch (ble_event->type_) {
@@ -525,6 +524,11 @@ void ESP32BLE::loop() {
esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param;
ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
#ifdef USE_ESP32_BLE_ADVERTISING
// A peripheral connect stops advertising, so drop a queued send; a handler that wants more refreshes again
if (event == ESP_GATTS_CONNECT_EVT && param->connect.link_role == 1)
this->advertising_cancel_start_();
#endif
this->gatts_event_callbacks_.call(event, gatts_if, param);
break;
}
@@ -614,6 +618,8 @@ void ESP32BLE::loop() {
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
}
// After the events, so refreshes they trigger go out in this pass
this->advertising_loop_();
}
void ESP32BLE::loop_handle_state_transition_not_active_() {
@@ -631,6 +637,9 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
return;
}
this->drain_ble_events_();
#ifdef USE_ESP32_BLE_ADVERTISING
this->advertising_cancel_start_();
#endif
// A status callback may have asked for BLE back; the stack is down now, so
// that request becomes a bring-up.
this->state_ =
+17
View File
@@ -112,6 +112,10 @@ class ESP32BLE final : public Component {
void get_mac_msb_first(uint8_t out[MAC_ADDRESS_SIZE]) const;
float get_setup_priority() const override;
void set_name(const char *name) { this->name_ = name; }
// BLE device names are limited to 20 characters
static constexpr size_t DEVICE_NAME_BUFFER_SIZE = 21;
/// The device name; written to `buffer` unless the configured name is used as is.
const char *device_name(char *buffer) const;
#ifdef USE_ESP32_BLE_ADVERTISING
/** Request advertising on behalf of a component.
@@ -131,6 +135,9 @@ class ESP32BLE final : public Component {
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);
void advertising_remove_service_uuid(ESPBTUUID uuid);
/// Keep `uuid` and the service data alone in the advertising packet; applies on the next refresh.
void advertising_set_priority_uuid(ESPBTUUID uuid);
void advertising_clear_priority_uuid();
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
#endif
@@ -188,8 +195,18 @@ class ESP32BLE final : public Component {
}
this->ble_events_.get_and_reset_dropped_count();
}
ESPHOME_ALWAYS_INLINE void advertising_loop_() {
#ifdef USE_ESP32_BLE_ADVERTISING
if (this->advertising_ != nullptr)
this->advertising_->loop();
#endif
}
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
void advertising_cancel_start_() {
if (this->advertising_ != nullptr)
this->advertising_->cancel_start();
}
#endif
// BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
@@ -3,31 +3,41 @@
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#include <cstdio>
#include <algorithm>
#include <cstring>
#include "esphome/components/ble_device_base/adv_payload.h"
#include "ble.h"
#include "ble_uuid.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifndef CONFIG_BT_CONTROLLER_DISABLED
#include <esp_bt.h>
#endif
namespace esphome::esp32_ble {
using ble_device_base::AdvPayload;
using ble_device_base::build_adv_packets;
ESPHOME_LOG_TAG(TAG, "esp32_ble.advertising");
BLEAdvertising::BLEAdvertising(uint32_t advertising_cycle_time) : advertising_cycle_time_(advertising_cycle_time) {
this->advertising_data_.set_scan_rsp = false;
this->advertising_data_.include_name = true;
this->advertising_data_.include_txpower = true;
this->advertising_data_.min_interval = 0;
this->advertising_data_.max_interval = 0;
this->advertising_data_.appearance = 0x00;
this->advertising_data_.manufacturer_len = 0;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.service_data_len = 0;
this->advertising_data_.p_service_data = nullptr;
this->advertising_data_.service_uuid_len = 0;
this->advertising_data_.p_service_uuid = nullptr;
this->advertising_data_.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
// The TX power Bluedroid advertised, from its BTM_BLE_ADV_TX_POWER table
static int8_t adv_tx_power_dbm() {
#ifdef CONFIG_IDF_TARGET_ESP32
static constexpr int8_t TX_POWER_DBM[] = {-12, -9, -6, -3, 0, 3, 6, 9};
#else
static constexpr int8_t TX_POWER_DBM[] = {-24, -21, -18, -15, -12, -9, -6, -3, 0, 3, 6, 9, 12, 15, 18, 20};
#endif
#ifdef CONFIG_BT_CONTROLLER_DISABLED
const size_t level = 0; // a hosted controller; Bluedroid sends level 0 too
#else
const size_t level = esp_ble_tx_power_get(ESP_BLE_PWR_TYPE_ADV);
#endif
return TX_POWER_DBM[std::min(level, std::size(TX_POWER_DBM) - 1)];
}
BLEAdvertising::BLEAdvertising(uint32_t advertising_cycle_time) : advertising_cycle_time_(advertising_cycle_time) {
this->advertising_params_.adv_int_min = 0x20;
this->advertising_params_.adv_int_max = 0x40;
this->advertising_params_.adv_type = ADV_TYPE_IND;
@@ -44,14 +54,9 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) {
}
void BLEAdvertising::set_service_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_service_data;
this->advertising_data_.p_service_data = nullptr;
this->advertising_data_.service_data_len = data.size();
if (!data.empty()) {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
this->advertising_data_.p_service_data = new uint8_t[data.size()];
memcpy(this->advertising_data_.p_service_data, data.data(), data.size());
}
// Too long to fit is the same as none
this->service_data_len_ = data.size() <= MAX_DATA_LEN ? data.size() : 0;
memcpy(this->service_data_.data(), data.data(), this->service_data_len_);
}
void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
@@ -59,69 +64,43 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();
if (!data.empty()) {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
this->advertising_data_.p_manufacturer_data = new uint8_t[data.size()];
memcpy(this->advertising_data_.p_manufacturer_data, data.data(), data.size());
}
// Only what fits goes on air
this->manufacturer_data_len_ = std::min(data.size(), MAX_DATA_LEN);
memcpy(this->manufacturer_data_.data(), data.data(), this->manufacturer_data_len_);
}
esp_err_t BLEAdvertising::services_advertisement_() {
int num_services = this->advertising_uuids_.size();
if (num_services == 0) {
this->advertising_data_.service_uuid_len = 0;
} else {
this->advertising_data_.service_uuid_len = 16 * num_services;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
this->advertising_data_.p_service_uuid = new uint8_t[this->advertising_data_.service_uuid_len];
uint8_t *p = this->advertising_data_.p_service_uuid;
for (int i = 0; i < num_services; i++) {
ESPBTUUID uuid = this->advertising_uuids_[i];
memcpy(p, uuid.as_128bit().get_uuid().uuid.uuid128, 16);
p += 16;
}
}
char name_buffer[ESP32BLE::DEVICE_NAME_BUFFER_SIZE];
const char *name = global_ble->device_name(name_buffer);
AdvPayload adv;
AdvPayload rsp;
build_adv_packets({.uuids = this->advertising_uuids_,
.priority_uuid = this->priority_uuid_,
.manufacturer_data = {this->manufacturer_data_.data(), this->manufacturer_data_len_},
.service_data = {this->service_data_.data(), this->service_data_len_},
.name = name,
.name_len = strlen(name),
.appearance = this->appearance_,
.tx_power_dbm = adv_tx_power_dbm(),
.name_in_adv = this->include_name_in_adv_},
adv, rsp);
esp_err_t err;
this->advertising_data_.set_scan_rsp = false;
this->advertising_data_.include_name = this->include_name_in_adv_ || !this->scan_response_;
this->advertising_data_.include_txpower = !this->scan_response_;
err = esp_ble_gap_config_adv_data(&this->advertising_data_);
// Bluedroid copies the bytes during each call
esp_err_t err = esp_ble_gap_config_adv_data_raw(const_cast<uint8_t *>(adv.data()), adv.size());
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %s", esp_err_to_name(err));
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err));
return err;
}
if (this->scan_response_) {
memcpy(&this->scan_response_data_, &this->advertising_data_, sizeof(esp_ble_adv_data_t));
this->scan_response_data_.set_scan_rsp = true;
this->scan_response_data_.include_name = true;
this->scan_response_data_.include_txpower = true;
this->scan_response_data_.manufacturer_len = 0;
this->scan_response_data_.appearance = 0;
this->scan_response_data_.flag = 0;
err = esp_ble_gap_config_adv_data(&this->scan_response_data_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Scan response): %s", esp_err_to_name(err));
return err;
}
err = esp_ble_gap_config_scan_rsp_data_raw(const_cast<uint8_t *>(rsp.data()), rsp.size());
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_scan_rsp_data_raw failed: %s", esp_err_to_name(err));
return err;
}
if (this->advertising_data_.service_uuid_len > 0) {
delete[] this->advertising_data_.p_service_uuid;
this->advertising_data_.p_service_uuid = nullptr;
}
err = esp_ble_gap_start_advertising(&this->advertising_params_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
@@ -134,6 +113,7 @@ void BLEAdvertising::start() {
}
void BLEAdvertising::stop() {
this->start_pending_ = false;
esp_err_t err = esp_ble_gap_stop_advertising();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_stop_advertising failed: %d", err);
@@ -145,6 +125,11 @@ void BLEAdvertising::stop() {
}
void BLEAdvertising::loop() {
// Send a queued start after the event drain
if (this->start_pending_) {
this->start_pending_ = false;
this->start();
}
if (this->raw_advertisements_callbacks_.empty()) {
return;
}
+18 -8
View File
@@ -12,7 +12,6 @@
#ifdef USE_ESP32_BLE_ADVERTISING
#include <esp_gap_ble_api.h>
#include <esp_gatts_api.h>
namespace esphome::esp32_ble {
@@ -24,25 +23,28 @@ class BLEAdvertising {
void add_service_uuid(ESPBTUUID uuid);
void remove_service_uuid(ESPBTUUID uuid);
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);
void set_include_name(bool include_name) { this->include_name_in_adv_ = include_name; }
/// Give `uuid` and the service data the advertising packet; the rest moves to the scan response.
void set_priority_uuid(ESPBTUUID uuid) { this->priority_uuid_ = uuid; }
void clear_priority_uuid() { this->priority_uuid_ = {}; }
void register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void start();
void stop();
/// Send from the next loop(): a burst of refreshes becomes one send instead of flooding the Bluetooth task queue.
void request_start() { this->start_pending_ = true; }
void cancel_start() { this->start_pending_ = false; }
protected:
// An element longer than this never fits in a 31 byte packet
static constexpr size_t MAX_DATA_LEN = 29;
esp_err_t services_advertisement_();
bool scan_response_;
bool include_name_in_adv_{false};
esp_ble_adv_data_t advertising_data_;
esp_ble_adv_data_t scan_response_data_;
esp_ble_adv_params_t advertising_params_;
std::vector<ESPBTUUID> advertising_uuids_;
@@ -50,7 +52,15 @@ class BLEAdvertising {
const uint32_t advertising_cycle_time_;
uint32_t last_advertisement_time_{0};
std::array<uint8_t, MAX_DATA_LEN> manufacturer_data_;
std::array<uint8_t, MAX_DATA_LEN> service_data_;
ESPBTUUID priority_uuid_; // unset when no service has priority
uint16_t appearance_{0};
int8_t current_adv_index_{-1}; // -1 means standard scan response
uint8_t manufacturer_data_len_{0};
uint8_t service_data_len_{0};
bool include_name_in_adv_{false};
bool start_pending_{false};
};
} // namespace esphome::esp32_ble
@@ -0,0 +1,38 @@
// Linker wraps that keep a bonded device's keys when it re-encrypts without pairing again (ESP-IDF 5.5.6).
// Same guard as espressif/esp-idf#19322; drop once ESPHome uses an ESP-IDF release that contains it.
// C, because Bluedroid's btm_ble_int.h does not close its extern "C" block.
// Compiled only with Bluedroid's private headers (bluedroid_headers_cmake); static analysis skips it
#ifdef USE_ESP32_BLE_BLUEDROID_HEADERS
#include <stdbool.h>
#include <stddef.h>
#include "btm_int.h"
#include "smp_int.h"
// The device record whose keys smp_check_auth_req() must keep, or NULL
static tBTM_SEC_DEV_REC *keep_keys_of = NULL;
void __real_smp_check_auth_req(tSMP_CB *p_cb, tSMP_INT_DATA *p_data);
void __real_btm_sec_clear_ble_keys(tBTM_SEC_DEV_REC *p_dev_rec);
// Runs on the Bluetooth host task. Encryption that succeeded without a Pairing Request or Response reused the bond.
void __wrap_smp_check_auth_req(tSMP_CB *p_cb, tSMP_INT_DATA *p_data) {
const bool reused_bond = *(UINT8 *) p_data == 1 && !(p_cb->flags & SMP_PAIR_FLAG_ENC_AFTER_PAIR);
// The same lookup as smp_check_auth_req(): it also matches a pseudo or resolvable private address
keep_keys_of = reused_bond ? btm_find_dev(p_cb->pairing_bda) : NULL;
__real_smp_check_auth_req(p_cb, p_data);
keep_keys_of = NULL;
}
void __wrap_btm_sec_clear_ble_keys(tBTM_SEC_DEV_REC *p_dev_rec) {
// Only the first clear, the one the upstream guard skips; later callers are not affected
if (keep_keys_of != NULL && p_dev_rec == keep_keys_of) {
keep_keys_of = NULL;
return;
}
__real_btm_sec_clear_ble_keys(p_dev_rec);
}
#endif // USE_ESP32_BLE_BLUEDROID_HEADERS
@@ -0,0 +1,30 @@
// Linker wrap that puts a GATT discovery response back in handle order before Bluedroid parses it.
// ESP-IDF 5.5.6 and 6.0.3 end the whole discovery when a peer lists handles out of order (esphome#22518), which
// earlier releases, Android and BlueZ accept.
#include "esphome/core/defines.h"
#ifdef USE_ESP32_BLE_GATTC_HANDLE_ORDER
#include "bluedroid_gattc_order.h"
namespace esphome::esp32_ble {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// tGATT_TCB is only passed through
void __real_gatt_client_handle_server_rsp(void *p_tcb, uint8_t op_code, uint16_t len, uint8_t *p_data,
uint16_t eatt_bearer_lcid);
// Runs on the Bluetooth host task
void __wrap_gatt_client_handle_server_rsp(void *p_tcb, uint8_t op_code, uint16_t len, uint8_t *p_data,
uint16_t eatt_bearer_lcid) {
esphome::esp32_ble::sort_att_response_by_handle(op_code, p_data, len);
__real_gatt_client_handle_server_rsp(p_tcb, op_code, len, p_data, eatt_bearer_lcid);
}
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP32_BLE_GATTC_HANDLE_ORDER
@@ -0,0 +1,57 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include "esphome/core/helpers.h"
namespace esphome::esp32_ble {
static constexpr uint8_t ATT_FIND_INFO_RSP = 0x05;
static constexpr uint8_t ATT_FIND_BY_TYPE_VALUE_RSP = 0x07;
static constexpr uint8_t ATT_READ_BY_TYPE_RSP = 0x09;
static constexpr uint8_t ATT_READ_BY_GRP_TYPE_RSP = 0x11;
static constexpr size_t ATT_MAX_MTU = 517;
/// Sort records of `size` bytes that each start with a little-endian handle, in place; a partial tail is left alone.
inline void sort_records_by_handle(uint8_t *records, size_t len, size_t size) {
const size_t count = len / size;
auto handle = [records, size](size_t i) { return encode_uint16(records[i * size + 1], records[i * size]); };
for (size_t i = 1; i < count; i++) {
const uint16_t current = handle(i);
size_t j = i;
while (j > 0 && handle(j - 1) > current)
j--;
if (j != i)
std::rotate(records + j * size, records + i * size, records + (i + 1) * size);
}
}
/// Sort the records of an ATT discovery response by handle, in place; `data` is the PDU after the opcode.
/// A body longer than any ATT_MTU allows is left for Bluedroid to reject, so a peer cannot make the sort costly.
inline void sort_att_response_by_handle(uint8_t op_code, uint8_t *data, size_t len) {
if (len < 1 || len >= ATT_MAX_MTU)
return;
switch (op_code) {
case ATT_READ_BY_TYPE_RSP:
case ATT_READ_BY_GRP_TYPE_RSP:
// Length byte, then records of that length
if (data[0] >= 2)
sort_records_by_handle(data + 1, len - 1, data[0]);
break;
case ATT_FIND_INFO_RSP:
// Format byte: handle and 16-bit UUID, or handle and 128-bit UUID
if (data[0] == 1 || data[0] == 2)
sort_records_by_handle(data + 1, len - 1, data[0] == 1 ? 4 : 18);
break;
case ATT_FIND_BY_TYPE_VALUE_RSP:
// Found handle and group end handle, no header
sort_records_by_handle(data, len, 4);
break;
default:
break;
}
}
} // namespace esphome::esp32_ble
@@ -3,14 +3,13 @@
#ifdef USE_ESP32
#include <cstring>
#include <esp_gatt_defs.h>
namespace esphome::esp32_ble_server {
BLE2902::BLE2902() : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2902)) {
this->value_.attr_len = 2;
uint8_t data[2] = {0, 0};
memcpy(this->value_.attr_value, data, 2);
// The client configuration value is always 2 bytes
BLE2902::BLE2902() : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG), 2) {
this->set_value({0, 0});
}
} // namespace esphome::esp32_ble_server
@@ -27,9 +27,11 @@ from esphome.components.esp32_ble import (
bt_uuid32_format,
bt_uuid128_format,
)
from esphome.components.wifi import DOMAIN as WIFI_DOMAIN
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTIVE,
CONF_AP,
CONF_CONTINUOUS,
CONF_DURATION,
CONF_ID,
@@ -78,6 +80,7 @@ def _get_required_features() -> set[BLEFeatures]:
# Slot counters sizing the tracker's StaticVector storage; one request per
# registered listener or client.
CLIENT_COUNT_DEFINE = "ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT"
SHORT_SCAN_WINDOW_DEFINE = "USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW"
_request_listener_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT")
_request_client_slot = cg.slot_counter(CLIENT_COUNT_DEFINE)
@@ -137,7 +140,9 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
# listens 9.4 % of the time and misses most advertisements when wifi shares
# the radio. Espressif recommends setting the window equal to the interval in
# that case: the coexistence arbiter still shares the radio with wifi, and
# BLE uses the airtime wifi does not claim.
# BLE uses the airtime wifi does not claim. Full duty leaves the AP no airtime
# for joins though, so while the AP runs the tracker drops back to the short
# window (esphome/esphome#22534).
IDF_SCAN_WINDOW_FIX_VERSION = cv.Version(5, 5, 5)
# Above this the scanner holds the shared radio long enough that wifi drops
@@ -181,6 +186,8 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
present iff wifi is configured and not disabled by the user) and the IDF
honors the window strictly (>= 5.5.5); without the arbiter a full-duty
scan would starve wifi outright, and a user-set window is never touched.
The raise also arms the short window used while a GATT connection or the
wifi AP is active.
Raising to the interval cannot invalidate the already-validated
parameters, so no re-validation is needed. The connection window is
checked against the window here, after the raise.
@@ -194,7 +201,7 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
# Copy so the config dump shows a plain value instead of a YAML
# anchor/alias pair pointing at the interval.
params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL])
# Arm the connection-time fallback unless the user set one. Injected
# Arm the connection and AP fallback unless the user set one. Injected
# after validation; safe because it equals the validated window default.
if CONF_CONNECTION_SCAN_WINDOW not in params:
params[CONF_CONNECTION_SCAN_WINDOW] = cv.positive_time_period(
@@ -353,20 +360,22 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_scan_interval(ble_device_base.to_ble_units(params[CONF_INTERVAL])))
cg.add(var.set_scan_window(ble_device_base.to_ble_units(params[CONF_WINDOW])))
if (connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW)) is not None:
# Emitted at FINAL so a scan-only build, where the guarded C++ path
# compiles out, skips the call entirely.
# Emitted at FINAL, once the client count is known: a build with
# neither clients nor a wifi AP compiles the short-window path out.
window_units = ble_device_base.to_ble_units(connection_window)
@coroutine_with_priority(CoroPriority.FINAL)
async def _emit_connection_scan_window() -> None:
if cg.get_slot_count(CLIENT_COUNT_DEFINE):
has_ap = CONF_AP in CORE.config.get(WIFI_DOMAIN, {})
if cg.get_slot_count(CLIENT_COUNT_DEFINE) or has_ap:
cg.add_define(SHORT_SCAN_WINDOW_DEFINE)
cg.add(var.set_connection_scan_window(window_units))
elif not _get_data().connection_window_injected:
# Warn only for a user-set value; the injected default drops silently.
_LOGGER.warning(
"'%s' has no effect because this build has no BLE client "
"components (for example bluetooth_proxy with active "
"connections, or ble_client)",
"connections, or ble_client) and no wifi access point",
CONF_CONNECTION_SCAN_WINDOW,
)
@@ -26,6 +26,10 @@
#include <esp_coexist.h>
#endif
#if defined(USE_WIFI_AP) && defined(USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW)
#include "esphome/components/wifi/wifi_component.h"
#endif
// bt_trace.h
#undef TAG
@@ -80,6 +84,15 @@ void ESP32BLETracker::loop() {
if (!this->parent_->is_active()) {
return;
}
#if defined(USE_WIFI_AP) && defined(USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW)
// The AP state is not versioned; fold its changes in so the window restart runs.
const bool ap_active = wifi::global_wifi_component->is_ap_active();
if (ap_active != this->ap_active_) {
this->ap_active_ = ap_active;
this->state_version_++;
}
#endif
if (this->ble_was_disabled_) {
this->ble_was_disabled_ = false;
// First start after boot or after the stack came back.
@@ -126,9 +139,10 @@ void ESP32BLETracker::loop() {
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
// - connection-window restart: scan_params_ is only written in start_scan_()
// (which changes scanner state via set_scanner_state_()), and
// counts.active/disconnecting only change on client state changes
// - short-window restart: scan_params_ is only written in start_scan_()
// (which changes scanner state via set_scanner_state_()),
// counts.active/disconnecting only change on client state changes, and
// ap_active_ changes bump state_version_ at the top of loop()
//
// All conditions that affect the logic below are tied to state changes that increment
// state_version_, so the fast path is safe.
@@ -152,11 +166,12 @@ void ESP32BLETracker::loop() {
this->handle_scanner_failure_();
}
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
// The programmed window no longer matches the connection state (typically
// the last connection dropped): restart so the right window applies now
// instead of at the end of the scan period. Continuous only (a user-started
// scan would not restart); !disconnecting matches the restart gate below.
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
// The programmed window no longer matches the connection or AP state
// (typically the last connection dropped, or the AP started or stopped):
// restart so the right window applies now instead of at the end of the scan
// period. Continuous only (a user-started scan would not restart);
// !disconnecting matches the restart gate below.
if (this->scanner_state_ == ScannerState::RUNNING && this->scan_continuous_ && !counts.disconnecting &&
this->scan_params_.scan_window != this->desired_scan_window_(counts.active)) {
// Same logical scan period continues: no on_scan_end sweeps for this
@@ -215,7 +230,7 @@ void ESP32BLETracker::stop_scan() {
// reason at D themselves, and the user-facing stop action is deliberate.
ESP_LOGV(TAG, "Stopping scan.");
this->scan_continuous_ = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
// The window-change restart is abandoned with continuous scanning.
this->skip_next_scan_end_ = false;
#endif
@@ -231,6 +246,8 @@ void ESP32BLETracker::ble_before_disabled_event_handler() {
for (auto *client : this->clients_) {
client->ble_before_disabled_event_handler();
}
#endif
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
this->skip_next_scan_end_ = false;
#endif
// The stop above never completes (stack torn down, events dropped); settle
@@ -277,7 +294,7 @@ void ESP32BLETracker::start_scan_(bool first) {
ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING.");
if (!first)
this->notify_scan_end_();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
this->skip_next_scan_end_ = false;
#endif
#ifdef USE_ESP32_BLE_DEVICE
@@ -287,12 +304,12 @@ void ESP32BLETracker::start_scan_(bool first) {
this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
this->scan_params_.scan_interval = this->scan_interval_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
// Count fresh: an automation can start a scan before loop() refreshes the counts.
const uint32_t window = this->desired_scan_window_(this->count_client_states_().active);
if (window != this->scan_window_) {
// Guarantee the connection airtime instead of scanning wall to wall.
ESP_LOGV(TAG, "Connection active, using %" PRIu32 " unit scan window", window);
// Leave airtime for the connection or the AP instead of scanning wall to wall.
ESP_LOGV(TAG, "Connection or AP active, using %" PRIu32 " unit scan window", window);
}
#else
const uint32_t window = this->scan_window_;
@@ -458,7 +475,7 @@ void ESP32BLETracker::dump_config() {
" Continuous Scanning: %s",
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
if (this->connection_scan_window_ != 0) {
ESP_LOGCONFIG(TAG, " Connection Scan Window: %.1f ms", this->connection_scan_window_ * 0.625f);
}
@@ -548,7 +565,7 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
}
void ESP32BLETracker::notify_scan_end_() {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
// Window-change restart continues the same scan period; the flag stays set
// across the stop and is cleared by the restart in start_scan_.
if (this->skip_next_scan_end_)
@@ -599,8 +616,10 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
}
ESP_LOGD(TAG, "Promoting client to connect");
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
// A connect ends the scan period a window-change restart was continuing.
this->skip_next_scan_end_ = false;
#endif
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(true);
#endif
@@ -172,7 +172,7 @@ class ESP32BLETracker final : public Component,
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; }
void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; }
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
void set_connection_scan_window(uint32_t scan_window) { connection_scan_window_ = scan_window; }
#endif
void set_scan_active(bool scan_active) { scan_active_ = scan_active; }
@@ -328,13 +328,15 @@ class ESP32BLETracker final : public Component,
uint32_t scan_duration_;
uint32_t scan_interval_;
uint32_t scan_window_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
/// Window used while a GATT connection is active; set by the user, or
/// defaulted when the window was raised to full duty (0 = no fallback).
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
/// Window used while a GATT connection or the wifi AP is active; set by the
/// user, or defaulted when the window was raised to full duty (0 = none).
uint32_t connection_scan_window_{0};
/// The window to scan at for the given number of active GATT connections.
/// The window to scan at for the given number of active GATT connections
/// and the last seen AP state.
uint32_t desired_scan_window_(uint8_t active) const {
return (this->connection_scan_window_ != 0 && active > 0) ? this->connection_scan_window_ : this->scan_window_;
return (this->connection_scan_window_ != 0 && (active > 0 || this->ap_active_)) ? this->connection_scan_window_
: this->scan_window_;
}
#endif
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
@@ -362,9 +364,11 @@ class ESP32BLETracker final : public Component,
#endif
bool ble_was_disabled_ : 1 {true};
bool parse_advertisements_ : 1 {false};
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
#ifdef USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
/// Suppress the window-change restart's on_scan_end sweeps (stop and start).
bool skip_next_scan_end_ : 1 {false};
/// Last seen wifi AP state, polled by loop(); stays false without an AP.
bool ap_active_ : 1 {false};
#endif
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_ : 1 {false};
-5
View File
@@ -4,8 +4,6 @@
#include <osapi.h>
#include <user_interface.h>
// for xt_rsil()/xt_wsr_ps()
#include <Arduino.h>
namespace esphome {
@@ -15,9 +13,6 @@ bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) =
// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2) is set,
// independent of the ESPHOME_THREAD_SINGLE thread model define.
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
// ESP8266 LwIPLock is defined inline as a no-op in helpers.h
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
+1 -1
View File
@@ -248,7 +248,7 @@ async def to_code(config: ConfigType) -> None:
)
wifi.enable_runtime_roaming_suppression()
esp32.add_idf_component(name="francescopace/espectre", ref="3.1.0")
esp32.add_idf_component(name="francescopace/espectre", ref="3.2.0")
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CSI_ENABLED", True)
# CSI is reported once per received transmission, so aggregation hides frames from sensing.
# Disabling TX aggregation also lets the SDK fix the station TX rate (6.5 Mbps on ESP32).
@@ -422,10 +422,15 @@ void ESPHomeOTAComponent::handle_handshake_() {
} else
#endif
{
// No password, move directly to data phase
// No password, move directly to data phase. Not a fallthrough: the next
// case is AUTH_SEND, which would demand a password that is not set.
this->transition_ota_state_(OTAState::DATA);
this->handle_data_();
return;
}
#ifdef USE_OTA_PASSWORD
[[fallthrough]];
#endif
}
#ifdef USE_OTA_PASSWORD
@@ -22,8 +22,9 @@ void GPIOOneWireBus::dump_config() {
this->dump_devices_(TAG);
}
// Lock only what must not stretch; the ESP8266 Wi-Fi interrupt handler asserts (hardware WDT) when held off ~1 ms.
// A longer gap between slots is within spec.
int HOT IRAM_ATTR GPIOOneWireBus::reset_int() {
InterruptLock lock;
// See reset here:
// https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
// Wait for communication to clear (delay G)
@@ -36,27 +37,31 @@ int HOT IRAM_ATTR GPIOOneWireBus::reset_int() {
} while (!this->pin_.digital_read());
bool r = false;
uint32_t start;
{
// Locked from the reset pulse through presence detection (at most about 780 us): a reset pulse stretched past
// 960 us power-resets a parasite-powered sensor
InterruptLock lock;
// Send 480µs LOW TX reset pulse (drive bus low, delay H)
this->pin_.digital_write(false);
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
delayMicroseconds(480);
// Send 480µs LOW TX reset pulse (drive bus low, delay H)
this->pin_.digital_write(false);
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
delayMicroseconds(480);
// Release the bus, delay I
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
start = micros();
delayMicroseconds(30);
// Release the bus, delay I
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
uint32_t start = micros();
delayMicroseconds(30);
while (micros() - start < 300) {
// sample bus, 0=device(s) present, 1=no device present
r = !this->pin_.digital_read();
if (r)
break;
delayMicroseconds(1);
while (micros() - start < 300) {
// sample bus, 0=device(s) present, 1=no device present
r = !this->pin_.digital_read();
if (r)
break;
delayMicroseconds(1);
}
}
// delay J: finish the 480us slot, but never spin if it already elapsed
// (unsigned wrap here would busy-wait for minutes with interrupts off)
// delay J: finish the 480us slot, but never spin if it already elapsed (unsigned wrap)
uint32_t elapsed = micros() - start;
if (elapsed < 480)
delayMicroseconds(480 - elapsed);
@@ -66,9 +71,6 @@ int HOT IRAM_ATTR GPIOOneWireBus::reset_int() {
}
void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) {
// drive bus low
this->pin_.digital_write(false);
// from datasheet:
// write 0 low time: t_low0: min=60µs, max=120µs
// write 1 low time: t_low1: min=1µs, max=15µs
@@ -78,27 +80,36 @@ void HOT IRAM_ATTR GPIOOneWireBus::write_bit_(bool bit) {
uint32_t delay0 = bit ? 6 : 60;
uint32_t delay1 = bit ? 64 : 10;
// delay A/C
delayMicroseconds(delay0);
// release bus
this->pin_.digital_write(true);
{
InterruptLock lock;
// drive bus low
this->pin_.digital_write(false);
// delay A/C
delayMicroseconds(delay0);
// release bus
this->pin_.digital_write(true);
}
// delay B/D
delayMicroseconds(delay1);
}
bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() {
// drive bus low
this->pin_.digital_write(false);
bool r;
{
InterruptLock lock;
// drive bus low
this->pin_.digital_write(false);
// datasheet says >= 1µs
delayMicroseconds(5);
// datasheet says >= 1µs
delayMicroseconds(5);
// release bus, delay E
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
// release bus, delay E
this->pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
delayMicroseconds(8);
// sample bus to read bit from peer
bool r = this->pin_.digital_read();
delayMicroseconds(8);
// sample bus to read bit from peer
r = this->pin_.digital_read();
}
// read slot is at least 60µs
delayMicroseconds(50);
@@ -109,21 +120,18 @@ bool HOT IRAM_ATTR GPIOOneWireBus::read_bit_() {
}
void IRAM_ATTR GPIOOneWireBus::write8(uint8_t val) {
InterruptLock lock;
for (uint8_t i = 0; i < 8; i++) {
this->write_bit_(bool((1u << i) & val));
}
}
void IRAM_ATTR GPIOOneWireBus::write64(uint64_t val) {
InterruptLock lock;
for (uint8_t i = 0; i < 64; i++) {
this->write_bit_(bool((1ULL << i) & val));
}
}
uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
InterruptLock lock;
uint8_t ret = 0;
for (uint8_t i = 0; i < 8; i++)
ret |= (uint8_t(this->read_bit_()) << i);
@@ -131,7 +139,6 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
}
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
InterruptLock lock;
uint64_t ret = 0;
for (uint8_t i = 0; i < 64; i++) {
ret |= (uint64_t(this->read_bit_()) << i);
@@ -146,7 +153,6 @@ void GPIOOneWireBus::reset_search() {
}
uint64_t IRAM_ATTR GPIOOneWireBus::search_int() {
InterruptLock lock;
if (this->last_device_flag_)
return 0u;
@@ -527,6 +527,8 @@ void ImprovBLEComponent::request_advertising_() {
if (this->advertising_requested_)
return;
this->advertising_requested_ = true;
// Passive scanners only see the advertising packet
esp32_ble::global_ble->advertising_set_priority_uuid(ESPBTUUID::from_raw(improv::SERVICE_UUID));
esp32_ble::global_ble->advertising_start();
}
@@ -534,6 +536,7 @@ void ImprovBLEComponent::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
esp32_ble::global_ble->advertising_clear_priority_uuid();
esp32_ble::global_ble->advertising_stop();
}
+17 -1
View File
@@ -12,7 +12,12 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import VARIANT_ESP32P4, only_on_variant
from esphome.components.esp32 import (
CONF_EXECUTE_FROM_PSRAM,
VARIANT_ESP32P4,
add_idf_sdkconfig_option,
only_on_variant,
)
from esphome.components.mipi import (
COLOR_ORDERS,
CONF_COLOR_DEPTH,
@@ -38,10 +43,12 @@ from esphome.components.mipi import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_FRAMEWORK,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
@@ -50,7 +57,9 @@ from esphome.const import (
CONF_ROTATION,
CONF_TRANSFORM,
CONF_WIDTH,
KEY_ESP32,
)
from esphome.core import CORE
from esphome.final_validate import full_config
from esphome.types import ConfigType
@@ -187,6 +196,13 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
# While flash is written (NVS, OTA) the cache is off; a DSI interrupt that waits for it makes the
# panel miss a frame, seen as a white flash. Code run from PSRAM does not wait for the flash.
if not CORE.config[KEY_ESP32][CONF_FRAMEWORK][CONF_ADVANCED][
CONF_EXECUTE_FROM_PSRAM
]:
add_idf_sdkconfig_option("CONFIG_LCD_DSI_ISR_CACHE_SAFE", True)
model = MODELS[config[CONF_MODEL].upper()]
color_depth = COLOR_DEPTHS[get_color_depth(config)]
pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit"))
+9 -1
View File
@@ -15,7 +15,15 @@ static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64;
// Highest AXI QoS priority (4 bits); all masters start at 0
static constexpr uint32_t DSI_DMA_READ_PRIORITY = 15;
static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) {
// With CONFIG_LCD_DSI_ISR_CACHE_SAFE the DPI driver only accepts callbacks in IRAM
#if CONFIG_LCD_DSI_ISR_CACHE_SAFE
#define MIPI_DSI_ISR_ATTR IRAM_ATTR
#else
#define MIPI_DSI_ISR_ATTR
#endif
static bool MIPI_DSI_ISR_ATTR notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata,
void *user_ctx) {
SemaphoreHandle_t sem = static_cast<SemaphoreHandle_t>(user_ctx);
BaseType_t need_yield = pdFALSE;
xSemaphoreGiveFromISR(sem, &need_yield);
@@ -1,4 +1,5 @@
#include "remote_transmitter.h"
#include <optional>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
@@ -8,6 +9,15 @@
namespace esphome::remote_transmitter {
ESPHOME_LOG_TAG(TAG, "remote_transmitter");
// Holding interrupts off about 1 ms makes the ESP8266 Wi-Fi SDK hang until a watchdog resets the chip, so a frame
// is not locked whole there: interrupts are on until EDGE_LOCK_US before each edge. Elsewhere micros() can jump
// when interrupts come back on, so the whole frame stays locked.
#ifdef USE_ESP8266
static constexpr bool LOCK_PER_EDGE = true;
#else
static constexpr bool LOCK_PER_EDGE = false;
#endif
static constexpr int32_t EDGE_LOCK_US = 50;
void RemoteTransmitterComponent::setup() {
this->pin_->setup();
@@ -35,44 +45,62 @@ void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequen
*off_time_period = period - *on_time_period;
}
void RemoteTransmitterComponent::await_target_time_() {
void RemoteTransmitterComponent::await_target_time_(int32_t lead_us) {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else if ((int32_t) (this->target_time_ - current_time) > 0) {
// busy loop is required as interrupts are disabled and delayMicroseconds()
} else if ((int32_t) (this->target_time_ - current_time) > lead_us) {
// busy loop, as interrupts can be disabled here and delayMicroseconds()
// may not work correctly in interrupt-disabled contexts on all platforms
while ((int32_t) (this->target_time_ - micros()) > 0)
while ((int32_t) (this->target_time_ - micros()) > lead_us)
;
}
}
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
void RemoteTransmitterComponent::write_at_target_(bool value) {
std::optional<InterruptLock> edge_lock;
if (LOCK_PER_EDGE) {
// The first edge anchors the frame, so it takes the time under the lock, right before the write
if (this->target_time_ != 0)
this->await_target_time_(EDGE_LOCK_US);
edge_lock.emplace();
}
this->await_target_time_();
this->pin_->digital_write(true);
this->pin_->digital_write(value);
}
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
this->write_at_target_(true);
const uint32_t target = this->target_time_ + usec;
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency
// When an interrupt made the falling edge late by a whole period or more, skip the periods it used up
// instead of catching up with runt pulses. Computed once from one timestamp; lateness under a period
// (call overhead at a fast carrier) and an on time that rounds to 0 are left alone. With a frame lock no
// interrupt can delay an edge, so the check is compiled out there.
if (LOCK_PER_EDGE && on_time > 0) {
const uint32_t period = on_time + off_time;
const uint32_t late = micros() - (this->target_time_ + on_time);
if ((int32_t) late >= (int32_t) period)
this->target_time_ += (late / period + 1) * period;
}
this->target_time_ += on_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(false);
this->write_at_target_(false);
this->target_time_ += off_time;
if ((int32_t) (this->target_time_ - target) >= 0)
break;
this->await_target_time_();
this->pin_->digital_write(true);
this->write_at_target_(true);
}
}
this->target_time_ = target;
}
void RemoteTransmitterComponent::space_(uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(false);
this->write_at_target_(false);
this->target_time_ += usec;
}
@@ -85,10 +113,13 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
this->transmit_trigger_.trigger();
for (uint32_t i = 0; i < send_times; i++) {
{
InterruptLock lock;
// Re-anchor every iteration: timing must never span a lock boundary, as micros() can
std::optional<InterruptLock> frame_lock;
if (!LOCK_PER_EDGE)
frame_lock.emplace();
// Re-anchor every iteration: outside ESP8266 timing must never span a lock boundary, as micros() can
// jump when interrupts are re-enabled between repeats (e.g. LibreTiny's Beken micros()
// discards its interrupt-lock correction, stretching the repeat gap by the lock duration)
// discards its interrupt-lock correction, stretching the repeat gap by the lock duration).
// ESP8266 micros() does not jump, so there the lock is taken per edge in write_at_target_().
this->target_time_ = 0;
for (int32_t item : this->temp_.get_data()) {
if (item > 0) {
@@ -100,8 +131,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
App.feed_wdt();
}
this->await_target_time_(); // wait for duration of last pulse
this->pin_->digital_write(false);
this->write_at_target_(false); // wait for duration of last pulse
}
if (i + 1 < send_times) {
@@ -95,7 +95,7 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
#if defined(USE_ESP8266) || \
(defined(USE_LIBRETINY) && !defined(USE_LIBRETINY_VARIANT_RTL8720C) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || \
defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
void await_target_time_();
void await_target_time_(int32_t lead_us = 0);
uint32_t target_time_{0};
#endif
#if defined(USE_ESP8266) || \
@@ -106,6 +106,7 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec);
void space_(uint32_t usec);
void write_at_target_(bool value);
#endif
#ifdef USE_RTL87XX
// Carrier frequency the PWM is currently configured for; 0 = not yet configured
@@ -133,12 +133,12 @@ void IRAM_ATTR RemoteTransmitterComponent::stop_envelope_timer_() { gtimer_stop(
#else // !USE_LIBRETINY_VARIANT_RTL8720C -- AmebaZ (RTL8710B): spin-based envelope, per-frame priority boost
void RemoteTransmitterComponent::await_target_time_() {
void RemoteTransmitterComponent::await_target_time_(int32_t lead_us) {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else {
while ((int32_t) (this->target_time_ - micros()) > 0) {
while ((int32_t) (this->target_time_ - micros()) > lead_us) {
}
}
}
+11 -10
View File
@@ -36,7 +36,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -601,25 +600,27 @@ async def to_code(config: ConfigType) -> None:
if data.player_support:
cg.add_define("USE_SENDSPIN_PLAYER", True)
# Configures the player role. Each configured codec is advertised for 16 bits per sample
# mono and stereo at the configured sample rate. The order is a preference order, both for
# the codecs themselves and for stereo over mono.
# Configures the player role. Each configured codec is advertised for mono and stereo at the
# configured sample rate and bits per sample, except Opus, which sendspin-cpp only decodes to
# 16 bits per sample. The order is a preference order, both for the codecs themselves and for
# stereo over mono.
player_cfg = data.player_config
sample_rate = player_cfg[CONF_SAMPLE_RATE]
bits_per_sample = player_cfg[CONF_BITS_PER_SAMPLE]
codecs = [CODECS[codec] for codec in player_cfg[CONF_CODECS]]
def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer:
def _audio_format(codec: str, channels: int) -> cg.StructInitializer:
return cg.StructInitializer(
AudioSupportedFormatObject,
("codec", codec),
("codec", CODECS[codec]),
("channels", channels),
("sample_rate", sample_rate),
("bit_depth", 16),
("bit_depth", 16 if codec == CODEC_OPUS else bits_per_sample),
)
audio_format_structs = [
_audio_format(codec, channels) for codec in codecs for channels in (2, 1)
_audio_format(codec, channels)
for codec in player_cfg[CONF_CODECS]
for channels in (2, 1)
]
psram_stack = player_cfg.get(CONF_TASK_STACK_IN_PSRAM, False)
@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import media_source, psram
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_BUFFER_SIZE,
CONF_ID,
CONF_SAMPLE_RATE,
@@ -74,6 +75,7 @@ def _register(config: ConfigType) -> ConfigType:
{
CONF_CODECS: config[CONF_CODECS],
CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE],
CONF_BITS_PER_SAMPLE: config[CONF_BITS_PER_SAMPLE],
CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE],
CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY],
CONF_FIXED_DELAY: config[CONF_FIXED_DELAY],
@@ -104,6 +106,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.int_range(
min=16000, max=96000
),
cv.Optional(CONF_BITS_PER_SAMPLE, default=16): cv.one_of(16, 24, int=True),
cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True),
cv.Optional(CONF_CODECS): cv.All(
cv.ensure_list(cv.enum(CODECS, lower=True)), cv.Length(min=1)
@@ -1,6 +1,7 @@
#ifdef USE_ESP8266
#include "uart_component_esp8266.h"
#include <esp8266_peri.h>
#include <optional>
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
@@ -21,6 +22,8 @@ static constexpr uint32_t SW_SERIAL_EDGE_MODE_MAX_BAUD = 19200;
// Edge decoder time base: 1/16 us from the 1 MHz system timer, which keeps its rate during a
// CpuFrequencyBoost; the cycle counter does not.
static constexpr uint32_t RX_TICKS_PER_SECOND = 16000000;
// Longest per-bit TX interrupt lock; the Wi-Fi interrupt handler asserts (hardware WDT) when held off about 1 ms
static constexpr uint32_t MAX_TX_LOCK_CYCLES = F_CPU / 1000000 * 200;
__attribute__((always_inline)) static inline uint32_t rx_now() { return micros() << 4; }
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -177,6 +180,11 @@ void ESP8266UartComponent::dump_config() {
this->baud_rate_ <= SW_SERIAL_EDGE_MODE_MAX_BAUD ? LOG_STR_LITERAL("edge")
: LOG_STR_LITERAL("start bit sampler"));
}
if (this->tx_pin_ != nullptr && this->baud_rate_ <= SW_SERIAL_EDGE_MODE_MAX_BAUD &&
this->sw_serial_->locks_whole_frame()) {
ESP_LOGW(TAG, " TX locks interrupts for whole bytes: another software UART receives above %u baud",
(unsigned) SW_SERIAL_EDGE_MODE_MAX_BAUD);
}
}
this->check_logger_conflict();
}
@@ -261,10 +269,20 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p
// load_settings() re-enters here
if (this->gpio_rx_pin_ != nullptr)
this->gpio_rx_pin_->detach_interrupt();
// This instance counted itself if its RX ran the whole-byte interrupt
if (this->lock_frame_ && this->gpio_rx_pin_ != nullptr)
blocking_rx_count--;
// RX disabled: forget the old pin, so a later reload does not remove this instance from the count again
if (rx_pin == nullptr)
this->gpio_rx_pin_ = nullptr;
this->bit_time_ = F_CPU / baud_rate;
this->stop_bits_ = stop_bits;
this->data_bits_ = data_bits;
this->parity_ = parity;
const bool edge_mode = baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD;
// Above the edge mode limit a bit is 52 us or less, so an interrupt between bits would stretch one; the whole frame
// is locked instead, well under 1 ms
this->lock_frame_ = !edge_mode;
// to_isr() allocates, so call it only for a new or changed pin
if (tx_pin != nullptr) {
if (tx_pin != this->gpio_tx_pin_)
@@ -286,10 +304,11 @@ void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_p
this->rx_.setup((RX_TICKS_PER_SECOND + baud_rate / 2) / baud_rate, data_bits, parity != UART_CONFIG_PARITY_NONE,
stop_bits, buffer, rx_buffer_size);
this->rx_.reset(rx_now(), this->rx_pin_.digital_read());
if (baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD) {
if (edge_mode) {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr_edge, this, gpio::INTERRUPT_ANY_EDGE);
} else {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
blocking_rx_count++;
}
}
}
@@ -369,27 +388,32 @@ void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
need_parity_bit = false;
}
// Transmit runs from the main loop and never overlaps a CpuFrequencyBoost
const uint32_t bit_time = this->bit_time_;
std::optional<InterruptLock> frame_lock;
if (this->locks_whole_frame())
frame_lock.emplace();
uint32_t start;
{
// Transmit runs from the main loop and never overlaps a CpuFrequencyBoost
InterruptLock lock;
const uint32_t bit_time = this->bit_time_;
uint32_t wait = bit_time;
const uint32_t start = arch_get_cpu_cycle_count();
// Start bit
this->write_bit_(false, &wait, start, bit_time);
for (int i = 0; i < this->data_bits_; i++) {
bool bit = data & (1 << i);
this->write_bit_(bit, &wait, start, bit_time);
if (need_parity_bit)
parity_bit ^= bit;
}
if (need_parity_bit)
this->write_bit_(parity_bit, &wait, start, bit_time);
// Stop bit
this->write_bit_(true, &wait, start, bit_time);
if (this->stop_bits_ == 2)
this->wait_(&wait, start, bit_time);
this->tx_pin_.digital_write(false);
start = arch_get_cpu_cycle_count();
}
uint32_t wait = bit_time;
for (int i = 0; i < this->data_bits_; i++) {
bool bit = data & (1 << i);
this->write_bit_(bit, &wait, start, bit_time);
if (need_parity_bit)
parity_bit ^= bit;
}
if (need_parity_bit)
this->write_bit_(parity_bit, &wait, start, bit_time);
// Stop bits: write_bit_ waits before its edge, so the stop bit is held with wait_
this->write_bit_(true, &wait, start, bit_time);
this->wait_(&wait, start, bit_time);
if (this->stop_bits_ == 2)
this->wait_(&wait, start, bit_time);
}
void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
while (arch_get_cpu_cycle_count() - start < *wait)
@@ -400,9 +424,14 @@ bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &
this->wait_(wait, start, bit_time);
return this->rx_pin_.digital_read();
}
// Locks only from MAX_TX_LOCK_CYCLES before the edge to the edge, so a pending interrupt runs right after it; a bit
// longer than that (below about 5000 baud) spins with interrupts on first. Nests inside a frame lock.
void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
this->tx_pin_.digital_write(bit);
while (arch_get_cpu_cycle_count() - start + MAX_TX_LOCK_CYCLES < *wait)
;
InterruptLock lock;
this->wait_(wait, start, bit_time);
this->tx_pin_.digital_write(bit);
}
uint8_t ESP8266SoftwareSerial::read_byte() {
this->rx_sync_();
@@ -26,6 +26,10 @@ class ESP8266SoftwareSerial {
size_t available();
/// TX holds interrupts off for whole frames: above the edge mode limit, or while any instance's RX interrupt reads
/// whole bytes (it must not run between TX bits).
bool locks_whole_frame() const { return this->lock_frame_ || blocking_rx_count != 0; }
protected:
/// Above SW_SERIAL_EDGE_MODE_MAX_BAUD; reads the whole byte in the ISR.
static void gpio_intr(ESP8266SoftwareSerial *arg);
@@ -51,6 +55,8 @@ class ESP8266SoftwareSerial {
UARTParityOptions parity_;
uint8_t stop_bits_;
uint8_t data_bits_;
bool lock_frame_{false};
static inline uint8_t blocking_rx_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
};
class ESP8266UartComponent final : public UARTComponent, public Component {
+14 -12
View File
@@ -707,6 +707,15 @@ void WiFiComponent::setup() {
}
}
void WiFiComponent::apply_radio_settings_() {
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
if (!this->wifi_apply_power_save_()) {
ESP_LOGV(TAG, "Setting Power Save Option failed");
}
}
void WiFiComponent::start() {
ESP_LOGCONFIG(TAG, "Starting");
// The loop's time, not millis(): a wifi.enable from a trigger in this pass must not stamp a time after now
@@ -750,10 +759,6 @@ void WiFiComponent::start() {
if (this->has_sta()) {
this->wifi_sta_pre_setup_();
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
// Synchronize power_save_ with semaphore state before applying
if (this->high_performance_semaphore_ != nullptr) {
@@ -767,9 +772,7 @@ void WiFiComponent::start() {
}
}
#endif
if (!this->wifi_apply_power_save_()) {
ESP_LOGV(TAG, "Setting Power Save Option failed");
}
this->apply_radio_settings_();
this->transition_to_phase_(WiFiRetryPhase::INITIAL_CONNECT);
#ifdef USE_WIFI_FAST_CONNECT
@@ -1119,11 +1122,10 @@ void WiFiComponent::setup_ap_config_() {
// Reset before the AP exists: afterwards only the platform's event path writes the counter
this->ap_clients_ = 0;
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
// The station path applied it in start(); without a station the radio only exists from here
if (this->ap_setup_ && !this->has_sta() && !std::isnan(this->output_power_) &&
!this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
// The station path applied these in start(); without a station the radio only exists from here,
// and credentials saved through the portal connect without another start()
if (this->ap_setup_ && !this->has_sta())
this->apply_radio_settings_();
#if defined(USE_WIFI_AP_EXCLUSIVE) || defined(USE_WIFI_AP_PERSISTENT)
// A failed start must not leave the radio in AP mode: on an exclusive radio it would refuse
// the STA attempts until the next try, and an AP that stays up would take clients it cannot serve.
+2
View File
@@ -847,6 +847,8 @@ class WiFiComponent final : public Component {
bool wifi_sta_pre_setup_();
bool wifi_apply_output_power_(float output_power);
bool wifi_apply_power_save_();
/// Output power and power save, wherever the radio first comes up: start() with networks, the AP without.
void apply_radio_settings_();
#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
bool wifi_apply_band_mode_();
#endif
+105 -13
View File
@@ -2,12 +2,15 @@
#ifdef USE_ESP32
#ifdef USE_ZIGBEE
#include <algorithm>
#include <cstdint>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
#include "zigbee_attribute_esp32.h"
#include "zigbee_esp32.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "zigbee_helpers_esp32.h"
#ifdef USE_WIFI
@@ -19,6 +22,16 @@ namespace esphome::zigbee {
ESPHOME_LOG_TAG(TAG, "zigbee");
static constexpr uint32_t COMMISSIONING_RETRY_TIMEOUT_ID = 0;
static constexpr uint32_t PARENT_LINK_TIMEOUT_ID = 1;
static constexpr uint32_t PARENT_LINK_RETRY_TIMEOUT_ID = 2;
// Since esp-zigbee-lib 2.0.5 every lost poll or transmission raises PARENT_LINK_FAILURE, even during a short radio
// outage such as a BLE scan. After a failure the device polls at least every 3 s. It rejoins only after failures in
// 4 polls that span at least 6 s; failures less than two thirds of a polling interval apart (poll retries, fast
// polls) count once. Two intervals without a counted failure mean that a poll got through.
static constexpr uint8_t PARENT_LINK_MISSED_POLLS = 4;
static constexpr uint32_t PARENT_LINK_POLL_INTERVAL_MS = 3000;
static constexpr uint32_t PARENT_LINK_MIN_SPAN_MS = 6000;
static ZigbeeComponent *global_zigbee = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -130,24 +143,16 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
esp_zigbee_factory_reset(); // triggers a reboot
}
global_zigbee->joined_ = false;
// For a leave asked for by the network: failures before it must not count after the rejoin
global_zigbee->reset_parent_link_failures_();
} break;
case EZB_NWK_SIGNAL_NETWORK_STATUS: {
const ezb_nwk_signal_network_status_params_t *network_status_params =
(const ezb_nwk_signal_network_status_params_t *) ezb_app_signal_get_params(app_signal);
if (network_status_params->status == EZB_NWK_NETWORK_STATUS_PARENT_LINK_FAILURE) {
global_zigbee->joined_ = false;
ESP_LOGW(TAG, "Parent link failure, attempting rejoin");
ezb_zdo_nwk_mgmt_leave_req_t leave_req = {
.dst_nwk_addr = ezb_nwk_get_short_address(),
.field =
{
.remove_children = false,
.rejoin = true,
},
};
// Send leave request to the network to rejoin
// triggers EZB_ZDO_SIGNAL_LEAVE signal first, then EZB_BDB_SIGNAL_DEVICE_REBOOT
ezb_zdo_nwk_mgmt_leave_req(&leave_req);
if (global_zigbee->joined_) { // else not joined yet, or a rejoin is running
global_zigbee->on_parent_link_failure_();
}
} else {
ESP_LOGD(TAG, "Zigbee APP Signal NETWORK_STATUS: 0x%02x", network_status_params->status);
}
@@ -159,6 +164,93 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
return true;
}
// Runs on the Zigbee task with the stack lock held
void ZigbeeComponent::on_parent_link_failure_() {
const uint32_t now = millis();
if (this->link_failures_ == 0) {
// Poll faster for now, so that a lost parent is noticed soon also with a long polling interval
this->saved_keep_alive_ = ezb_nwk_get_keepalive_interval();
if (this->saved_keep_alive_ > PARENT_LINK_POLL_INTERVAL_MS) {
ezb_nwk_set_keepalive_interval(PARENT_LINK_POLL_INTERVAL_MS);
}
}
// The polling interval during the series
const uint32_t interval = std::min(this->saved_keep_alive_, PARENT_LINK_POLL_INTERVAL_MS);
if (this->link_failures_ > 0) {
if (now - this->last_link_failure_ms_ >= 2 * interval) {
// The series has ended, but its timeout has not run yet: start a new one, keep the saved interval
this->link_failures_ = 0;
} else if (now - this->last_link_failure_ms_ < 2 * interval / 3) {
ESP_LOGV(TAG, "Parent link failure (same poll)");
return;
}
}
if (this->link_failures_ == 0) {
this->first_link_failure_ms_ = now;
}
this->last_link_failure_ms_ = now;
this->link_failure_seq_++;
if (this->link_failures_ < UINT8_MAX) {
this->link_failures_++;
}
if (this->link_failures_ < PARENT_LINK_MISSED_POLLS || now - this->first_link_failure_ms_ < PARENT_LINK_MIN_SPAN_MS) {
ESP_LOGD(TAG, "Parent link failure %u", this->link_failures_);
// The stack does not report a poll that got through, so the series ends when no failure follows
const uint8_t seq = this->link_failure_seq_;
this->set_timeout(PARENT_LINK_TIMEOUT_ID, 2 * interval, [this, seq]() { this->end_parent_link_failures_(seq); });
return;
}
ESP_LOGW(TAG, "Parent link failed in %u polls, attempting rejoin", this->link_failures_);
this->cancel_timeout(PARENT_LINK_TIMEOUT_ID);
this->reset_parent_link_failures_();
this->joined_ = false;
ezb_zdo_nwk_mgmt_leave_req_t leave_req = {
.dst_nwk_addr = ezb_nwk_get_short_address(),
.field =
{
.remove_children = false,
.rejoin = true,
},
};
// Send leave request to the network to rejoin
// triggers EZB_ZDO_SIGNAL_LEAVE signal first, then EZB_BDB_SIGNAL_DEVICE_REBOOT
if (ezb_zdo_nwk_mgmt_leave_req(&leave_req) != EZB_ERR_NONE) {
ESP_LOGW(TAG, "Leave request for rejoin failed");
this->joined_ = true; // still joined, so later failures can start a rejoin again
}
}
// Runs on the main loop when no failure was counted for two poll intervals
void ZigbeeComponent::end_parent_link_failures_(uint8_t seq) {
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
// Retry with its own id, so that it cannot replace the timeout of a newer failure
this->set_timeout(PARENT_LINK_RETRY_TIMEOUT_ID, 100, [this, seq]() { this->end_parent_link_failures_(seq); });
return;
}
// A newer failure has set its own timeout, which ends the series instead
const uint8_t failures = seq == this->link_failure_seq_ ? this->link_failures_ : 0;
if (failures > 0) {
this->reset_parent_link_failures_();
}
esp_zigbee_lock_release();
if (failures > 0) { // after the release: the main task writes log lines out at once
ESP_LOGD(TAG, "Parent link failures ended (%u)", failures);
}
}
// Ends the current series, if any. Needs the stack lock
void ZigbeeComponent::reset_parent_link_failures_() {
if (this->link_failures_ == 0) {
return; // no series: the saved interval is stale
}
this->link_failures_ = 0;
// Put the polling interval back, unless something else changed it in the meantime
if (this->saved_keep_alive_ > PARENT_LINK_POLL_INTERVAL_MS &&
ezb_nwk_get_keepalive_interval() == PARENT_LINK_POLL_INTERVAL_MS) {
ezb_nwk_set_keepalive_interval(this->saved_keep_alive_);
}
}
void load_zb_event(ZBEvent *event, ezb_zcl_message_info_t info, ezb_zcl_attribute_t attribute) {
event->load_set_attr_value_event(info, attribute);
}
+9
View File
@@ -90,6 +90,9 @@ class ZigbeeComponent final : public Component {
ezb_nwk_device_type_t device_role_ = EZB_NWK_DEVICE_TYPE_ROUTER;
#endif
void update_basic_cluster_(ezb_af_ep_desc_t ep_desc);
void on_parent_link_failure_();
void end_parent_link_failures_(uint8_t seq);
void reset_parent_link_failures_();
template<typename T>
void add_attr_(ZigbeeAttribute *attr, uint8_t endpoint_id, uint16_t cluster_id, uint8_t role, uint16_t attr_id,
T *value_p);
@@ -115,6 +118,12 @@ class ZigbeeComponent final : public Component {
std::atomic<bool> joined_ = false;
std::atomic<bool> join_pending_ = false;
std::atomic<bool> factory_new_ = false;
// Current series of parent link failures, only used with the Zigbee stack lock held
uint32_t first_link_failure_ms_{0};
uint32_t last_link_failure_ms_{0};
uint32_t saved_keep_alive_{0};
uint8_t link_failures_{0};
uint8_t link_failure_seq_{0};
};
template<typename T>
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.11.0b3"
__version__ = "2026.11.0b4"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+3
View File
@@ -507,7 +507,9 @@
#define USE_ESP32_LOG_V2
#define USE_WIFI_SCAN_RESULTS_LOCK
#define USE_ESP32_BLE
#define USE_ESP32_BLE_BLUEDROID_BOND_KEYS
#define USE_ESP32_BLE_BLUEDROID_HEX_WRAPS
#define USE_ESP32_BLE_GATTC_HANDLE_ORDER
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
#define USE_ESP32_BLE_CLIENT
#define USE_ESP32_BLE_DEVICE
@@ -531,6 +533,7 @@
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 2, 0)
#define USE_ESP32_BLE_TRACKER_DIRECT_CONN_GUARD
#endif
#define USE_ESP32_BLE_TRACKER_SHORT_SCAN_WINDOW
#define USE_BLE_GATT_CLIENT
#define ESPHOME_BLE_GATT_CLIENT_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
+7
View File
@@ -29,6 +29,7 @@
#ifdef USE_ESP8266
#include <Esp.h>
#include <core_esp8266_features.h> // xt_rsil, xt_wsr_ps
#include <pgmspace.h>
#endif
@@ -2249,8 +2250,14 @@ class LockGuard {
*/
class InterruptLock {
public:
#ifdef USE_ESP8266
// Inline: saves two calls per lock, and the lock sits on bit-banged timing paths
ESPHOME_ALWAYS_INLINE InterruptLock() : state_(xt_rsil(15)) {}
ESPHOME_ALWAYS_INLINE ~InterruptLock() { xt_wsr_ps(this->state_); }
#else
InterruptLock();
~InterruptLock();
#endif
protected:
#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR)
+1 -1
View File
@@ -46,7 +46,7 @@ dependencies:
rules:
- if: "target in [esp32h2, esp32p4]"
francescopace/espectre:
version: "3.1.0"
version: "3.2.0"
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32c3, esp32c5, esp32c6]"
zorxx/multipart-parser:
+1 -1
View File
@@ -37,7 +37,7 @@ lib_deps_base =
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier
esphome/dsmr_parser@1.10.0 ; dsmr
esphome/dsmr_parser@1.11.0 ; dsmr
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
@@ -0,0 +1,70 @@
"""Tests for the Bluedroid wraps that keep bond keys on re-encryption."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.esp32.const import KEY_ESP32, KEY_SRC_CMAKE
from esphome.components.esp32_ble import bluedroid_headers_cmake
from esphome.core import CORE
_WRAP_FLAGS = {
"-Wl,--wrap=smp_check_auth_req",
"-Wl,--undefined=__wrap_smp_check_auth_req",
"-Wl,--wrap=btm_sec_clear_ble_keys",
"-Wl,--undefined=__wrap_btm_sec_clear_ble_keys",
}
@pytest.mark.parametrize(
("framework", "windows", "expected"),
[
pytest.param("{type: esp-idf, version: 5.5.6}", False, True, id="idf_5_5_6"),
pytest.param("{type: esp-idf, version: 5.5.5}", False, False, id="idf_5_5_5"),
pytest.param("{type: esp-idf, version: 6.0.1}", False, False, id="idf_6"),
pytest.param("{type: esp-idf, version: 5.5.6}", True, False, id="windows"),
# Arduino 3.3.12 is built on ESP-IDF 5.5.6, 3.3.11 on 5.5.5
pytest.param(
"{type: arduino, version: 3.3.12}", False, True, id="arduino_3_3_12"
),
pytest.param(
"{type: arduino, version: 3.3.11}", False, False, id="arduino_3_3_11"
),
# PlatformIO builds cannot reach the private headers
pytest.param(
"{type: esp-idf, version: 5.5.6}\n toolchain: platformio",
False,
False,
id="platformio_idf_5_5_6",
),
pytest.param(
"{type: arduino, version: 3.3.12}\n toolchain: platformio",
False,
False,
id="platformio_arduino_3_3_12",
),
],
)
def test_wraps_only_on_idf_5_5_6_native_builds(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
framework: str,
windows: bool,
expected: bool,
) -> None:
monkeypatch.setattr("esphome.components.esp32_ble.IS_WINDOWS", windows)
config = tmp_path / "test.yaml"
config.write_text(
"esphome:\n name: test\n\nesp32:\n variant: esp32\n"
f" framework: {framework}\n\nesp32_ble: {{}}\n"
)
generate_main(config)
assert CORE.build_flags & _WRAP_FLAGS == (_WRAP_FLAGS if expected else set())
defines = {define.name for define in CORE.defines}
assert ("USE_ESP32_BLE_BLUEDROID_BOND_KEYS" in defines) is expected
headers = bluedroid_headers_cmake("bluedroid_bond_keys.c")
assert (headers in CORE.data[KEY_ESP32][KEY_SRC_CMAKE]) is expected
@@ -0,0 +1,93 @@
"""Tests for the wrap that sorts GATT discovery responses by handle."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.esp32_ble import _has_strict_handle_order
import esphome.config_validation as cv
from esphome.core import CORE
_WRAP_FLAGS = {
"-Wl,--wrap=gatt_client_handle_server_rsp",
"-Wl,--undefined=__wrap_gatt_client_handle_server_rsp",
}
_CLIENTS = {
"ble_client": (
"esp32_ble_tracker: {}\n\n"
"ble_client:\n - mac_address: AA:BB:CC:DD:EE:FF\n id: client\n"
),
# The proxy connects through bluetooth_connection, without esp32_ble_client
"bluetooth_proxy": (
"wifi:\n ssid: test\n password: password1\n\napi: {}\n\n"
"bluetooth_proxy:\n active: true\n"
),
"none": "",
}
@pytest.mark.parametrize(
("version", "expected"),
[
("5.4.4", False),
("5.5.5", False),
("5.5.6", True),
("5.5.7", True),
("6.0.0", False),
("6.0.2", False),
("6.0.3", True),
("6.1.0", True),
],
)
def test_strict_handle_order_versions(version: str, expected: bool) -> None:
"""ESP-IDF added the check in 5.5.6 and 6.0.3."""
assert _has_strict_handle_order(cv.Version.parse(version)) is expected
@pytest.mark.parametrize(
("framework", "client", "expected"),
[
pytest.param(
"{type: esp-idf, version: 5.5.6}", "ble_client", True, id="idf_5_5_6"
),
pytest.param(
"{type: esp-idf, version: 5.5.6}", "bluetooth_proxy", True, id="proxy"
),
pytest.param(
"{type: esp-idf, version: 5.5.5}", "ble_client", False, id="idf_5_5_5"
),
pytest.param(
"{type: esp-idf, version: 6.0.1}", "ble_client", False, id="idf_6_0_1"
),
pytest.param("{type: esp-idf, version: 5.5.6}", "none", False, id="no_client"),
# Arduino 3.3.12 is built on ESP-IDF 5.5.6; no private headers are needed
pytest.param(
"{type: arduino, version: 3.3.12}", "ble_client", True, id="arduino"
),
pytest.param(
"{type: esp-idf, version: 5.5.6}\n toolchain: platformio",
"ble_client",
True,
id="platformio",
),
],
)
def test_wrap_only_with_a_gatt_client_on_strict_idf(
generate_main: Callable[[str | Path], str],
tmp_path: Path,
framework: str,
client: str,
expected: bool,
) -> None:
config = tmp_path / "test.yaml"
config.write_text(
"esphome:\n name: test\n\nesp32:\n variant: esp32\n"
f" framework: {framework}\n\nesp32_ble: {{}}\n\n" + _CLIENTS[client]
)
generate_main(config)
assert CORE.build_flags & _WRAP_FLAGS == (_WRAP_FLAGS if expected else set())
defines = {define.name for define in CORE.defines}
assert ("USE_ESP32_BLE_GATTC_HANDLE_ORDER" in defines) is expected
@@ -8,7 +8,7 @@ from pathlib import Path
import pytest
from esphome.components.esp32.const import KEY_ESP32, KEY_SRC_CMAKE
from esphome.components.esp32_ble import BLUEDROID_HEADERS_CMAKE
from esphome.components.esp32_ble import bluedroid_headers_cmake
from esphome.core import CORE
_WRAP_FLAGS = {
@@ -51,4 +51,5 @@ def test_wraps_only_on_newlib_idf_without_rom_sscanf(
defines = {define.name for define in CORE.defines}
assert ("USE_ESP32_BLE_BLUEDROID_HEX_WRAPS" in defines) is expected
# The drift checks need Bluedroid's private headers
assert (BLUEDROID_HEADERS_CMAKE in CORE.data[KEY_ESP32][KEY_SRC_CMAKE]) is expected
headers = bluedroid_headers_cmake("bluedroid_hex_wraps.cpp")
assert (headers in CORE.data[KEY_ESP32][KEY_SRC_CMAKE]) is expected
@@ -0,0 +1,13 @@
esphome:
name: scan-window-scan-only-ap
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
ap:
esp32_ble_tracker:
@@ -0,0 +1,15 @@
esphome:
name: scan-window-user-scan-only-ap
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
ap:
esp32_ble_tracker:
scan_parameters:
connection_scan_window: 20ms
@@ -24,6 +24,7 @@ from esphome.components.esp32 import KEY_IDF_VERSION
from esphome.components.esp32_ble_tracker import (
CONF_SOFTWARE_COEXISTENCE,
CONFIG_SCHEMA,
SHORT_SCAN_WINDOW_DEFINE,
)
from esphome.const import CONF_INTERVAL, PlatformFramework
from esphome.core import CORE
@@ -200,13 +201,17 @@ def test_connection_scan_window_truncation_collapse_rejected(
("config_file", "window_call", "connection_call", "warns"),
[
# Raised window with GATT clients: the injected fallback is emitted.
("scan_window_raised.yaml", "set_scan_window(512)", True, False),
("scan_window_raised.yaml", "set_scan_window(512)", 48, False),
# Explicit window: nothing injected.
("scan_window_explicit.yaml", "set_scan_window(48)", False, False),
# Scan-only build compiles the path out: the injected default is
# dropped silently, a user-set value warns.
("scan_window_scan_only.yaml", "set_scan_window(512)", False, False),
("scan_window_user_set_scan_only.yaml", "set_scan_window(512)", False, True),
("scan_window_explicit.yaml", "set_scan_window(48)", None, False),
# Scan-only build without a wifi AP compiles the path out: the injected
# default is dropped silently, a user-set value warns.
("scan_window_scan_only.yaml", "set_scan_window(512)", None, False),
("scan_window_user_set_scan_only.yaml", "set_scan_window(512)", None, True),
# A wifi AP needs the short window even without clients
# (esphome/esphome#22534).
("scan_window_scan_only_ap.yaml", "set_scan_window(512)", 48, False),
("scan_window_user_set_scan_only_ap.yaml", "set_scan_window(512)", 32, False),
],
)
def test_connection_scan_window_codegen(
@@ -215,12 +220,17 @@ def test_connection_scan_window_codegen(
caplog: pytest.LogCaptureFixture,
config_file: str,
window_call: str,
connection_call: bool,
connection_call: int | None,
warns: bool,
) -> None:
main_cpp = generate_main(component_config_path(config_file))
assert window_call in main_cpp
assert ("set_connection_scan_window(48)" in main_cpp) == connection_call
armed = any(define.name == SHORT_SCAN_WINDOW_DEFINE for define in CORE.defines)
assert armed == (connection_call is not None)
if connection_call is None:
assert "set_connection_scan_window(" not in main_cpp
else:
assert f"set_connection_scan_window({connection_call})" in main_cpp
assert ("'connection_scan_window' has no effect" in caplog.text) == warns
@@ -0,0 +1,21 @@
esphome:
name: p4-xip-test
esp32:
board: esp32-p4-evboard
framework:
type: esp-idf
advanced:
execute_from_psram: true
psram:
speed: 200MHz
esp_ldo:
- channel: 3
voltage: 2.5V
display:
- platform: mipi_dsi
id: p4_nano
model: WAVESHARE-P4-NANO-10.1
@@ -8,7 +8,12 @@ import re
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4, VARIANT_ESP32S3
from esphome.components.esp32 import (
KEY_BOARD,
KEY_SDKCONFIG_OPTIONS,
VARIANT_ESP32P4,
VARIANT_ESP32S3,
)
# Importing the I/O expanders registers their pin schemas with
# pins.PIN_SCHEMA_REGISTRY so that models whose defaults reference expander-backed
@@ -21,9 +26,11 @@ from esphome.const import (
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_WIDTH,
KEY_ESP32,
KEY_VARIANT,
PlatformFramework,
)
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
@@ -217,6 +224,26 @@ def test_code_generation(
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
def test_isr_cache_safe(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""The DSI interrupts run while flash is written, so the panel does not miss a frame."""
generate_main(component_fixture_path("mipi_dsi.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_LCD_DSI_ISR_CACHE_SAFE") is True
def test_isr_cache_safe_not_set_when_executing_from_psram(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""Code run from PSRAM does not wait for the flash, so the option is not needed."""
generate_main(component_fixture_path("execute_from_psram.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert "CONFIG_LCD_DSI_ISR_CACHE_SAFE" not in sdkconfig
def test_unsupported_variant_reported_before_requirements(
set_core_config: SetCoreConfigCallable,
) -> None:
@@ -0,0 +1,16 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ap:
sendspin:
media_source:
- platform: sendspin
bits_per_sample: 24
@@ -0,0 +1,15 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ap:
sendspin:
media_source:
- platform: sendspin
@@ -1,9 +1,13 @@
"""Validation tests for the sendspin media_source platform.
"""Validation and code generation tests for the sendspin media_source platform.
These cover the codec preference list, whose rejection branches a compile test
cannot reach: a `test*.yaml` can only assert that a configuration is accepted.
cannot reach, and the advertised audio formats, which a compile test cannot
check: a `test*.yaml` can only assert that a configuration is accepted.
"""
from collections.abc import Callable
from pathlib import Path
import re
from typing import Any
import pytest
@@ -15,6 +19,13 @@ from esphome.const import PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
AUDIO_FORMAT_RE = re.compile(
r"\.codec = sendspin::SendspinCodecFormat::(\w+),\s*"
r"\.channels = (\d+),\s*"
r"\.sample_rate = \d+,\s*"
r"\.bit_depth = (\d+)"
)
def _media_source_config(**overrides: Any) -> ConfigType:
"""Build a minimal valid media source config, allowing field overrides."""
@@ -98,3 +109,33 @@ def test_opus_only_codec_list_rejected(
with pytest.raises(cv.Invalid, match="must include 'flac' or 'pcm'"):
CONFIG_SCHEMA(_media_source_config(codecs=["opus"]))
@pytest.mark.parametrize(
("config_file", "bits_per_sample"),
[
("media_source_defaults.yaml", 16),
("media_source_bits_per_sample_24.yaml", 24),
],
)
def test_advertised_bit_depths(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
bits_per_sample: int,
) -> None:
"""FLAC and PCM use the configured depth; Opus is only decoded to 16 bits."""
main_cpp = generate_main(component_config_path(config_file))
formats = [
(codec, int(channels), int(bit_depth))
for codec, channels, bit_depth in AUDIO_FORMAT_RE.findall(main_cpp)
]
assert formats == [
("FLAC", 2, bits_per_sample),
("FLAC", 1, bits_per_sample),
("OPUS", 2, 16),
("OPUS", 1, 16),
("PCM", 2, bits_per_sample),
("PCM", 1, bits_per_sample),
]
@@ -0,0 +1,235 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <span>
#include <vector>
#include "esphome/components/ble_device_base/adv_payload.h"
#include "esphome/core/helpers.h"
namespace esphome::ble_device_base::testing {
namespace {
constexpr uint8_t FLAGS = 0x06;
// 00467768-6228-2272-4663-277478268000, little endian
constexpr std::array<uint8_t, 16> IMPROV_RAW = {0x00, 0x80, 0x26, 0x78, 0x74, 0x27, 0x63, 0x46,
0x72, 0x22, 0x28, 0x62, 0x68, 0x77, 0x46, 0x00};
ESPBTUUID improv_uuid() { return ESPBTUUID::from_raw(IMPROV_RAW.data()); }
// A 16 or 32-bit UUID written out in 128-bit Bluetooth base form
ESPBTUUID base_uuid(uint32_t value) {
std::array<uint8_t, 16> raw = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00};
put_le32(raw.data() + 12, value);
return ESPBTUUID::from_raw(raw.data());
}
std::vector<uint8_t> bytes(const AdvPayload &payload) { return {payload.data(), payload.data() + payload.size()}; }
constexpr uint8_t MANUFACTURER[] = {0x4C, 0x00, 0x23, 0x77};
constexpr uint8_t IMPROV_DATA[] = {0x77, 0x46, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
AdvContent improv_server_content(std::span<const ESPBTUUID> uuids, bool priority) {
return {.uuids = uuids,
.priority_uuid = priority ? improv_uuid() : ESPBTUUID{},
.manufacturer_data = MANUFACTURER,
.service_data = IMPROV_DATA,
.name = "esp",
.name_len = 3,
.appearance = 0x0340,
.tx_power_dbm = -3,
.name_in_adv = false};
}
std::vector<uint8_t> concat(std::initializer_list<std::vector<uint8_t>> parts) {
std::vector<uint8_t> out;
for (const auto &part : parts)
out.insert(out.end(), part.begin(), part.end());
return out;
}
const std::vector<uint8_t> IMPROV_UUID_ELEMENT = concat({{0x11, 0x07}, {IMPROV_RAW.begin(), IMPROV_RAW.end()}});
const std::vector<uint8_t> IMPROV_DATA_ELEMENT =
concat({{0x09, 0x16}, {std::begin(IMPROV_DATA), std::end(IMPROV_DATA)}});
const std::vector<uint8_t> FLAGS_ELEMENT = {0x02, 0x01, FLAGS};
const std::vector<uint8_t> APPEARANCE_ELEMENT = {0x03, 0x19, 0x40, 0x03};
const std::vector<uint8_t> NAME_ELEMENT = {0x04, 0x09, 'e', 's', 'p'};
const std::vector<uint8_t> MANUFACTURER_ELEMENT = {0x05, 0xFF, 0x4C, 0x00, 0x23, 0x77};
const std::vector<uint8_t> TX_POWER_ELEMENT = {0x02, 0x0A, 0xFD};
const std::vector<uint8_t> UUID16_ELEMENT = {0x03, 0x03, 0x0A, 0x18};
} // namespace
TEST(AdvPayload, UuidSize) {
EXPECT_EQ(AdvPayload::uuid_size(ESPBTUUID::from_uint16(0x180F)), 2u);
EXPECT_EQ(AdvPayload::uuid_size(ESPBTUUID::from_uint32(0x180F)), 2u);
EXPECT_EQ(AdvPayload::uuid_size(ESPBTUUID::from_uint32(0x12345678)), 4u);
EXPECT_EQ(AdvPayload::uuid_size(base_uuid(0x180F)), 2u);
EXPECT_EQ(AdvPayload::uuid_size(base_uuid(0x12345678)), 4u);
EXPECT_EQ(AdvPayload::uuid_size(improv_uuid()), 16u);
}
TEST(AdvPayload, ServiceDataThatDoesNotFitIsDropped) {
const uint8_t service_data[9] = {};
AdvPayload adv;
adv.add_flags(FLAGS);
const ESPBTUUID uuid = improv_uuid();
adv.add_uuids({&uuid, 1});
adv.add_service_data(service_data);
EXPECT_EQ(adv.size(), 3u + 18u);
}
TEST(AdvPayload, LongNameIsShortened) {
const char *name = "a-very-long-device-name-over-31";
AdvPayload adv;
adv.add_flags(FLAGS);
adv.add_name(name, strlen(name));
ASSERT_EQ(adv.size(), AdvPayload::MAX_LEN);
EXPECT_EQ(adv.data()[3], 27);
EXPECT_EQ(adv.data()[4], 0x08);
EXPECT_EQ(memcmp(adv.data() + 5, name, 26), 0);
}
TEST(AdvPayload, NameAndTxPower) {
AdvPayload rsp;
rsp.add_name("esp", 3);
rsp.add_tx_power(-3);
EXPECT_EQ(bytes(rsp), (std::vector<uint8_t>{0x04, 0x09, 'e', 's', 'p', 0x02, 0x0A, 0xFD}));
}
TEST(AdvPayload, ManufacturerDataIsTruncated) {
std::vector<uint8_t> data(40, 0xAB);
AdvPayload adv;
adv.add_flags(FLAGS);
adv.add_appearance(0x0340);
adv.add_manufacturer_data(data);
ASSERT_EQ(adv.size(), AdvPayload::MAX_LEN);
EXPECT_EQ(adv.data()[3], 0x03);
EXPECT_EQ(adv.data()[4], 0x19);
EXPECT_EQ(adv.data()[5], 0x40);
EXPECT_EQ(adv.data()[6], 0x03);
EXPECT_EQ(adv.data()[7], 23); // 22 bytes of data
EXPECT_EQ(adv.data()[8], 0xFF);
}
TEST(AdvPayload, AppearanceNeedsFourBytes) {
AdvPayload adv;
adv.add_name("abcdefghijklmnopqrstuvwxyz", 26); // leaves 3 bytes
adv.add_appearance(0x0340);
EXPECT_EQ(adv.size(), 28u);
adv.add_appearance(0);
EXPECT_EQ(adv.size(), 28u);
}
TEST(AdvPayload, UuidListsByTypeThenFirst128) {
const ESPBTUUID uuids[] = {improv_uuid(), ESPBTUUID::from_uint16(0x180F), base_uuid(0x12345678), base_uuid(0x180A)};
AdvPayload adv;
adv.add_uuids(uuids);
EXPECT_EQ(bytes(adv), (std::vector<uint8_t>{0x05, 0x03, 0x0F, 0x18, 0x0A, 0x18, 0x05, 0x05, 0x78, 0x56,
0x34, 0x12, 0x11, 0x07, 0x00, 0x80, 0x26, 0x78, 0x74, 0x27,
0x63, 0x46, 0x72, 0x22, 0x28, 0x62, 0x68, 0x77, 0x46, 0x00}));
}
TEST(AdvPayload, Only128BitUuidWhenItFits) {
const ESPBTUUID uuids[] = {improv_uuid(), improv_uuid()};
AdvPayload adv;
adv.add_flags(FLAGS);
adv.add_name("0123456789", 10); // leaves 16 bytes
adv.add_uuids(uuids);
EXPECT_EQ(adv.size(), 15u);
}
TEST(AdvPayload, Partial16BitList) {
std::array<ESPBTUUID, 16> uuids;
for (size_t i = 0; i < uuids.size(); i++)
uuids[i] = ESPBTUUID::from_uint16(static_cast<uint16_t>(0x1800 + i));
AdvPayload adv;
adv.add_flags(FLAGS);
adv.add_uuids(uuids);
ASSERT_EQ(adv.size(), 3u + 2u + 13u * 2u);
EXPECT_EQ(adv.data()[3], 27);
EXPECT_EQ(adv.data()[4], 0x02);
EXPECT_EQ(adv.data()[5], 0x00);
EXPECT_EQ(adv.data()[6], 0x18);
}
TEST(AdvPayload, NoEmpty16BitList) {
const ESPBTUUID uuids[] = {ESPBTUUID::from_uint16(0x180A)};
AdvPayload adv;
adv.add_name("abcdefghijklmnopqrstuvwxyz", 26); // leaves 3 bytes
const auto before = bytes(adv);
adv.add_uuids(uuids);
EXPECT_EQ(bytes(adv), before);
}
TEST(AdvPayload, NoEmpty32BitList) {
const ESPBTUUID uuids[] = {ESPBTUUID::from_uint32(0x12345678)};
AdvPayload adv;
adv.add_name("abcdefghijklmnopqrstuvwx", 24); // leaves 5 bytes
const auto before = bytes(adv);
adv.add_uuids(uuids);
EXPECT_EQ(bytes(adv), before);
}
TEST(AdvPayload, ShortUuidAfterLongListsStillFits) {
// 15 custom 128-bit and 16 32-bit UUIDs ahead of a 16-bit one, which goes first on air
std::array<ESPBTUUID, 15 + 16 + 1> uuids;
for (size_t i = 0; i < 15; i++)
uuids[i] = improv_uuid();
for (size_t i = 0; i < 16; i++)
uuids[15 + i] = ESPBTUUID::from_uint32(static_cast<uint32_t>(0x12340000 + i));
uuids.back() = ESPBTUUID::from_uint16(0x180A);
AdvPayload adv;
adv.add_flags(FLAGS);
adv.add_uuids(uuids);
EXPECT_EQ(adv.data()[3], 0x03);
EXPECT_EQ(adv.data()[4], 0x03);
EXPECT_EQ(adv.data()[5], 0x0A);
EXPECT_EQ(adv.data()[6], 0x18);
EXPECT_EQ(adv.data()[8], 0x04); // partial 32-bit list
}
TEST(AdvPayload, SkipLeavesOutOneUuid) {
const ESPBTUUID uuids[] = {improv_uuid(), ESPBTUUID::from_uint16(0x180A)};
AdvPayload rsp;
rsp.add_uuids(uuids, improv_uuid());
EXPECT_EQ(bytes(rsp), (std::vector<uint8_t>{0x03, 0x03, 0x0A, 0x18}));
}
TEST(BuildAdvPackets, PriorityUuidOwnsTheAdvertisingPacket) {
const ESPBTUUID uuids[] = {improv_uuid(), ESPBTUUID::from_uint16(0x180A)};
AdvPayload adv;
AdvPayload rsp;
build_adv_packets(improv_server_content(uuids, true), adv, rsp);
EXPECT_EQ(bytes(adv), concat({FLAGS_ELEMENT, IMPROV_UUID_ELEMENT, IMPROV_DATA_ELEMENT}));
EXPECT_EQ(adv.size(), AdvPayload::MAX_LEN);
EXPECT_EQ(bytes(rsp),
concat({APPEARANCE_ELEMENT, NAME_ELEMENT, MANUFACTURER_ELEMENT, TX_POWER_ELEMENT, UUID16_ELEMENT}));
}
TEST(BuildAdvPackets, WithoutPriorityScanResponseRepeatsUuidsAndServiceData) {
const ESPBTUUID uuids[] = {ESPBTUUID::from_uint16(0x180A), improv_uuid()};
AdvPayload adv;
AdvPayload rsp;
build_adv_packets(improv_server_content(uuids, false), adv, rsp);
// No room for the 128-bit UUID in the advertising packet, nor for the service data in the scan response
EXPECT_EQ(bytes(adv),
concat({FLAGS_ELEMENT, APPEARANCE_ELEMENT, MANUFACTURER_ELEMENT, UUID16_ELEMENT, IMPROV_DATA_ELEMENT}));
EXPECT_EQ(bytes(rsp), concat({NAME_ELEMENT, TX_POWER_ELEMENT, UUID16_ELEMENT, IMPROV_UUID_ELEMENT}));
}
TEST(BuildAdvPackets, NameInAdvertisingPacket) {
AdvContent content = improv_server_content({}, false);
content.manufacturer_data = {};
content.service_data = {};
content.appearance = 0;
content.name_in_adv = true;
AdvPayload adv;
AdvPayload rsp;
build_adv_packets(content, adv, rsp);
EXPECT_EQ(bytes(adv), concat({FLAGS_ELEMENT, NAME_ELEMENT}));
EXPECT_EQ(bytes(rsp), concat({NAME_ELEMENT, TX_POWER_ELEMENT}));
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,148 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <initializer_list>
#include <vector>
#include "esphome/components/esp32_ble/bluedroid_gattc_order.h"
namespace esphome::esp32_ble::testing {
namespace {
void push_handle(std::vector<uint8_t> &pdu, uint16_t handle) {
pdu.insert(pdu.end(), {uint8_t(handle), uint8_t(handle >> 8)});
}
// Read By Type for characteristic discovery: 7 byte records of handle, properties, value handle, UUID16
std::vector<uint8_t> characteristics(std::initializer_list<uint16_t> handles) {
std::vector<uint8_t> pdu{7};
for (uint16_t h : handles) {
push_handle(pdu, h);
pdu.push_back(0x02);
push_handle(pdu, h + 1);
pdu.insert(pdu.end(), {uint8_t(h), 0x2a});
}
return pdu;
}
// Find Information: format byte, then handle and UUID16 (format 1) or handle and UUID128 (format 2)
std::vector<uint8_t> descriptors(uint8_t format, std::initializer_list<uint16_t> handles) {
std::vector<uint8_t> pdu{format};
for (uint16_t h : handles) {
push_handle(pdu, h);
pdu.insert(pdu.end(), format == 1 ? 2 : 16, uint8_t(h));
}
return pdu;
}
// Find By Type Value: found handle and group end handle, no header
std::vector<uint8_t> service_ranges(std::initializer_list<uint16_t> starts) {
std::vector<uint8_t> pdu;
for (uint16_t s : starts) {
push_handle(pdu, s);
push_handle(pdu, s + 0x0f);
}
return pdu;
}
void sort(uint8_t op_code, std::vector<uint8_t> &pdu) { sort_att_response_by_handle(op_code, pdu.data(), pdu.size()); }
} // namespace
// The records the walking pad in esphome#22518 sent for its Device Information service
TEST(SortAttResponseByHandle, OutOfOrderRecordsAreSortedWhole) {
auto pdu = characteristics({0x0109, 0x0107, 0x010B});
sort(ATT_READ_BY_TYPE_RSP, pdu);
EXPECT_EQ(pdu, characteristics({0x0107, 0x0109, 0x010B}));
}
TEST(SortAttResponseByHandle, ReverseOrderIsSorted) {
auto pdu = characteristics({0x0300, 0x0200, 0x0100, 0x0010});
sort(ATT_READ_BY_TYPE_RSP, pdu);
EXPECT_EQ(pdu, characteristics({0x0010, 0x0100, 0x0200, 0x0300}));
}
TEST(SortAttResponseByHandle, OrderedAndSingleRecordsAreUntouched) {
auto ordered = characteristics({0x0010, 0x0012, 0x0014});
auto single = characteristics({0x0042});
const auto ordered_before = ordered;
const auto single_before = single;
sort(ATT_READ_BY_TYPE_RSP, ordered);
sort(ATT_READ_BY_TYPE_RSP, single);
EXPECT_EQ(ordered, ordered_before);
EXPECT_EQ(single, single_before);
}
// Bluedroid ignores a partial last record, so it must stay where it is
TEST(SortAttResponseByHandle, PartialTailStaysInPlace) {
auto pdu = characteristics({0x0109, 0x0107});
pdu.insert(pdu.end(), {0x01, 0x01, 0x02});
sort(ATT_READ_BY_TYPE_RSP, pdu);
auto expected = characteristics({0x0107, 0x0109});
expected.insert(expected.end(), {0x01, 0x01, 0x02});
EXPECT_EQ(pdu, expected);
}
// A record too short to hold a handle, an unknown Find Information format or an empty PDU is left for Bluedroid
TEST(SortAttResponseByHandle, MalformedResponsesAreUntouched) {
std::vector<uint8_t> short_records{1, 0x09, 0x07};
auto bad_format = descriptors(1, {0x0109, 0x0107});
bad_format[0] = 3;
const auto short_before = short_records;
const auto format_before = bad_format;
sort(ATT_READ_BY_TYPE_RSP, short_records);
sort(ATT_FIND_INFO_RSP, bad_format);
EXPECT_EQ(short_records, short_before);
EXPECT_EQ(bad_format, format_before);
sort_att_response_by_handle(ATT_READ_BY_TYPE_RSP, nullptr, 0);
}
// Read By Group Type (service discovery) records start with the service's start handle too
TEST(SortAttResponseByHandle, GroupRecordsSortByStartHandle) {
std::vector<uint8_t> pdu{6, 0x20, 0x00, 0x2f, 0x00, 0x0d, 0x18, 0x01, 0x00, 0x1f, 0x00, 0x00, 0x18};
sort(ATT_READ_BY_GRP_TYPE_RSP, pdu);
EXPECT_EQ(pdu, (std::vector<uint8_t>{6, 0x01, 0x00, 0x1f, 0x00, 0x00, 0x18, 0x20, 0x00, 0x2f, 0x00, 0x0d, 0x18}));
}
// Descriptor discovery uses Find Information, which ESP-IDF 5.5.6 checks the same way
TEST(SortAttResponseByHandle, FindInformationSortsBothFormats) {
auto uuid16 = descriptors(1, {0x010A, 0x0108, 0x0109});
auto uuid128 = descriptors(2, {0x0122, 0x0121});
sort(ATT_FIND_INFO_RSP, uuid16);
sort(ATT_FIND_INFO_RSP, uuid128);
EXPECT_EQ(uuid16, descriptors(1, {0x0108, 0x0109, 0x010A}));
EXPECT_EQ(uuid128, descriptors(2, {0x0121, 0x0122}));
}
// Service discovery by UUID uses Find By Type Value, which has no header byte
TEST(SortAttResponseByHandle, FindByTypeValueSortsServiceRanges) {
auto pdu = service_ranges({0x0030, 0x0010, 0x0020});
sort(ATT_FIND_BY_TYPE_VALUE_RSP, pdu);
EXPECT_EQ(pdu, service_ranges({0x0010, 0x0020, 0x0030}));
}
// No ATT_MTU allows more than 516 bytes after the opcode; Bluedroid rejects longer bodies, so they are not sorted
TEST(SortAttResponseByHandle, OversizedBodiesAreLeftForBluedroid) {
std::vector<uint8_t> largest{2};
for (uint16_t h = 257; h > 0; h--)
push_handle(largest, h);
largest.push_back(0x00); // partial last record
ASSERT_EQ(largest.size(), ATT_MAX_MTU - 1);
auto oversized = largest;
oversized.push_back(0x00);
const auto oversized_before = oversized;
sort(ATT_READ_BY_TYPE_RSP, largest);
sort(ATT_READ_BY_TYPE_RSP, oversized);
EXPECT_EQ(encode_uint16(largest[2], largest[1]), 1);
EXPECT_EQ(oversized, oversized_before);
}
TEST(SortAttResponseByHandle, OtherResponsesAreUntouched) {
auto pdu = characteristics({0x0109, 0x0107});
const auto before = pdu;
sort(0x0B, pdu); // Read Response
EXPECT_EQ(pdu, before);
}
} // namespace esphome::esp32_ble::testing
@@ -1,3 +1,12 @@
# Scan-only tracker with a wifi AP: also builds the short scan window path
# without any BLE clients.
wifi:
ssid: MySSID
password: password1
ap:
esp32_ble_tracker:
scan_parameters:
connection_scan_window: 30ms
on_scan_end:
- logger.log: "Scan ended!"
@@ -5,6 +5,7 @@ media_source:
- platform: sendspin
id: media_source_id
buffer_size: 500000
bits_per_sample: 24
initial_static_delay: 5ms
static_delay_adjustable: true
fixed_delay: 480us
+1
View File
@@ -15,5 +15,6 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config: ConfigType) -> None:
if any(Path(p) == TESTS_DIR for p in CORE.config[CONF_ESPHOME][CONF_INCLUDES]):
cg.add_define("USE_WIFI")
cg.add_define("USE_WIFI_AP")
manifest.to_code = to_code_testing
@@ -0,0 +1,90 @@
#include <gtest/gtest.h>
#include <vector>
#include "esphome/components/wifi/wifi_component.h"
#include "esphome/core/application.h"
#include "host_hooks.h"
namespace esphome::wifi::testing {
namespace {
constexpr uint32_t AP_TIMEOUT_MS = 10000;
class WiFiAPPowerSaveTest : public ::testing::Test {
protected:
void SetUp() override {
host_hooks = HostHooks{};
WiFiAP ap;
ap.set_ssid("fallback");
this->wifi_.set_ap(ap);
}
void TearDown() override { global_wifi_component = this->previous_global_; }
void add_network_() {
WiFiAP sta;
sta.set_ssid("home");
this->wifi_.add_sta(sta);
}
WiFiComponent *previous_global_{global_wifi_component};
WiFiComponent wifi_;
};
// Credentials saved through the portal connect without another start(), so the AP must apply it
TEST_F(WiFiAPPowerSaveTest, APWithoutNetworksAppliesPowerSave) {
this->wifi_.start();
EXPECT_EQ(host_hooks.ap_starts, 1);
EXPECT_EQ(host_hooks.power_save_applies, 1);
}
TEST_F(WiFiAPPowerSaveTest, FailedAPStartDoesNotApplyPowerSave) {
host_hooks.ap_start_ok = false;
this->wifi_.start();
EXPECT_EQ(host_hooks.ap_starts, 1);
EXPECT_EQ(host_hooks.power_save_applies, 0);
}
TEST_F(WiFiAPPowerSaveTest, FallbackAPLeavesItToTheStationPath) {
this->add_network_();
this->wifi_.set_ap_timeout(AP_TIMEOUT_MS);
this->wifi_.start();
ASSERT_EQ(host_hooks.power_save_applies, 1);
LoopBlockingGuard clock{nullptr, nullptr, App.get_loop_component_start_time() + AP_TIMEOUT_MS + 1};
this->wifi_.loop();
EXPECT_EQ(host_hooks.ap_starts, 1);
EXPECT_EQ(host_hooks.power_save_applies, 1);
}
TEST_F(WiFiAPPowerSaveTest, APWithoutNetworksAppliesOutputPower) {
this->wifi_.set_output_power(17.5f);
this->wifi_.start();
EXPECT_EQ(host_hooks.output_powers, std::vector<float>{17.5f});
}
TEST_F(WiFiAPPowerSaveTest, NetworksApplyOutputPower) {
this->add_network_();
this->wifi_.set_output_power(17.5f);
this->wifi_.start();
EXPECT_EQ(host_hooks.output_powers, std::vector<float>{17.5f});
}
TEST_F(WiFiAPPowerSaveTest, UnsetOutputPowerIsNotApplied) {
this->wifi_.start();
this->add_network_();
this->wifi_.start();
EXPECT_TRUE(host_hooks.output_powers.empty());
}
} // namespace
} // namespace esphome::wifi::testing
+13 -2
View File
@@ -17,8 +17,14 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
return true;
}
bool WiFiComponent::wifi_sta_pre_setup_() { return true; }
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
bool WiFiComponent::wifi_apply_power_save_() { return true; }
bool WiFiComponent::wifi_apply_output_power_(float output_power) {
host_hooks.output_powers.push_back(output_power);
return true;
}
bool WiFiComponent::wifi_apply_power_save_() {
host_hooks.power_save_applies++;
return true;
}
bool WiFiComponent::wifi_apply_hostname_() { return true; }
void WiFiComponent::wifi_pre_setup_() {}
bool WiFiComponent::wifi_loop_() { return false; }
@@ -39,6 +45,11 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
return true;
}
bool WiFiComponent::wifi_disconnect_() { return true; }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
host_hooks.ap_starts++;
return host_hooks.ap_start_ok;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {}; }
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { return {}; }
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
+7
View File
@@ -14,6 +14,13 @@ struct HostHooks {
WiFiAP last_connect{};
/// Times the station was switched off, as restart_adapter() does.
int station_offs{0};
/// What the next AP start returns, and how many were tried.
bool ap_start_ok{true};
int ap_starts{0};
/// Output power values applied, in order.
std::vector<float> output_powers;
/// Times wifi_apply_power_save_() ran.
int power_save_applies{0};
};
extern HostHooks host_hooks; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -0,0 +1,14 @@
esphome:
name: host-ota-test
on_boot:
then:
- lambda: 'id(my_ota).set_auth_password("");'
host:
api:
ota:
- platform: esphome
id: my_ota
port: __OTA_PORT__
password: "hunter2"
logger:
level: DEBUG
+25
View File
@@ -556,6 +556,31 @@ async def test_host_ota_api_key_offer_with_password(
)
@pytest.mark.asyncio
async def test_host_ota_cleared_password(
yaml_config: str,
write_yaml_config: ConfigWriter,
compile_esphome: CompileFunction,
reserved_tcp_port: tuple[int, socket.socket],
) -> None:
"""A password cleared at boot with set_auth_password("") must not be
asked for: the auth path is compiled in but the device skips it."""
dev = _Device(
*await _build(
yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port
)
)
async with run_binary(
dev.binary_path, symbols_path=dev.symbols_path, line_callback=dev.on_log
) as (proc, lines):
dev.proc = proc
await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT)
await _wait_for_line(lines, "Over-The-Air updates")
assert not any("Password configured" in line for line in lines)
await dev.ota(None, None, "upload without a password must succeed")
@pytest.mark.asyncio
@pytest.mark.usefixtures("isolated_preferences")
async def test_host_ota_provisioned_api_key(