mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
34
Commits
while-yield
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03870b551 | ||
|
|
4a965b8b32 | ||
|
|
d826a19eb1 | ||
|
|
fb54ec51c1 | ||
|
|
04b6c0d4ca | ||
|
|
abf282dd08 | ||
|
|
e097958ad9 | ||
|
|
8a8ad4d8da | ||
|
|
09b82388c9 | ||
|
|
ea5407925a | ||
|
|
97732df3a5 | ||
|
|
04482ae0d4 | ||
|
|
ca0dd8e292 | ||
|
|
e75ac64cc9 | ||
|
|
212c0c1ea4 | ||
|
|
3e2f9bf296 | ||
|
|
b997866e86 | ||
|
|
ae89db98e4 | ||
|
|
e9bb102c1f | ||
|
|
26c4f8d40e | ||
|
|
c097fe388e | ||
|
|
21267d7421 | ||
|
|
582adc950a | ||
|
|
613319f984 | ||
|
|
4ad3cc7c0e | ||
|
|
a033260639 | ||
|
|
24f23ba3db | ||
|
|
674d593f75 | ||
|
|
c1771e002b | ||
|
|
5529e87a40 | ||
|
|
54cae78a42 | ||
|
|
e77a3bc54d | ||
|
|
af7f95a01b | ||
|
|
daf48ce4f2 |
No files matched your search
@@ -201,6 +201,9 @@ virtual listener base (a second vptr per entity).
|
||||
- `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`, `spi.new_spi_component`,
|
||||
`modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`) constructs, registers and
|
||||
attaches in one call.
|
||||
- A fixed buffer sized from the config is a file-scope static (BSS, not heap):
|
||||
`buf = cg.static_buffer(config[CONF_ID], cg.uint8, size)`, named after its owner so `esphome analyze-memory`
|
||||
counts it there; pass `buf` to the constructor.
|
||||
- `display.add_writer(var, config)` sets the lambda.
|
||||
- `MyEnum.options("on", prefix="MY_ENUM_")` builds an enum dict.
|
||||
- `button.restart_button_schema(cls)` (and `factory_reset_`, `query_`) are button presets.
|
||||
|
||||
@@ -42,6 +42,8 @@ APIs.
|
||||
// Or if size is constant:
|
||||
std::array<uint8_t, 256> buffer;
|
||||
```
|
||||
When the size comes from the config, declare it with `cg.static_buffer()` in `to_code` and pass the pointer
|
||||
to the constructor, so it lands in BSS (see the `component-codegen` skill).
|
||||
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** use `StaticVector` from
|
||||
`esphome/core/helpers.h` for compile-time fixed size with a `push_back()` interface (no dynamic
|
||||
|
||||
@@ -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.0b2
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
shared_progmem_array,
|
||||
shared_progmem_string,
|
||||
statement,
|
||||
static_buffer,
|
||||
static_const_array,
|
||||
static_function,
|
||||
templatable,
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
@@ -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_ =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -233,8 +233,8 @@ void HT16K33CharDisplay::set_blink(uint8_t blink) {
|
||||
this->send_(this->display_setup_command_());
|
||||
}
|
||||
|
||||
void HT16K33CharDisplay::display_off(bool turn_off) {
|
||||
assign_bits(this->display_setup_, DISPLAY_ON, !turn_off);
|
||||
void HT16K33CharDisplay::display_power(bool power_status) {
|
||||
assign_bits(this->display_setup_, DISPLAY_ON, power_status);
|
||||
this->send_(this->display_setup_command_());
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ class HT16K33CharDisplay final : public PollingComponent, public i2c::I2CDevice
|
||||
|
||||
/// 0 steady, 1 at 2 Hz, 2 at 1 Hz, 3 at 0.5 Hz; before setup only the value is kept
|
||||
void set_blink(uint8_t blink);
|
||||
void display_off(bool turn_off);
|
||||
/// Turn the display on or off
|
||||
void display_power(bool power_status);
|
||||
/// Stop the oscillator, the lowest power state; before setup only the value is kept
|
||||
void display_standby(bool standby);
|
||||
/// Clear the display memory; the next update redraws the message
|
||||
|
||||
@@ -325,6 +325,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
# Re-enable the I2C driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_i2c")
|
||||
if get_esp32_variant() == VARIANT_ESP32:
|
||||
# ACK_ERR interrupt storm after a NACK; see i2c_bus_esp_idf.cpp
|
||||
cg.add_define("USE_I2C_ISR_GUARD")
|
||||
cg.add_build_flag("-Wl,--wrap=esp_intr_alloc_intrstatus")
|
||||
if CORE.is_host:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -10,6 +10,70 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_I2C_ISR_GUARD
|
||||
#include <esp_intr_alloc.h>
|
||||
#include <hal/i2c_ll.h> // declares i2c_periph_signal (IDF 5: soc/, IDF 6: hal/)
|
||||
#if SOC_I2C_SUPPORT_HW_FSM_RST || I2C_LL_SUPPORT_HW_FSM_RST
|
||||
#error "This chip has an I2C FSM reset; the ISR guard is no longer needed"
|
||||
#endif
|
||||
|
||||
namespace esphome::i2c {
|
||||
// Without an FSM reset ACK_ERR can stay raised and the IDF ISR never masks it (esp-idf#17690). Mask the master
|
||||
// events until the next transaction when an error is raised both before and after the handler clears it.
|
||||
static constexpr uint32_t I2C_ERROR_INTR = I2C_LL_INTR_NACK | I2C_LL_INTR_TIMEOUT | I2C_LL_INTR_ARBITRATION;
|
||||
static_assert((I2C_LL_MASTER_EVENT_INTR & I2C_ERROR_INTR) == I2C_ERROR_INTR,
|
||||
"The master event mask must cover the error interrupts");
|
||||
|
||||
struct I2CIsrGuard {
|
||||
intr_handler_t handler;
|
||||
void *arg;
|
||||
i2c_dev_t *dev;
|
||||
};
|
||||
static I2CIsrGuard i2c_isr_guards[SOC_HP_I2C_NUM]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static void IRAM_ATTR i2c_isr_guard(void *arg) {
|
||||
auto *guard = static_cast<I2CIsrGuard *>(arg);
|
||||
uint32_t before;
|
||||
i2c_ll_get_intr_mask(guard->dev, &before);
|
||||
guard->handler(guard->arg);
|
||||
uint32_t after;
|
||||
i2c_ll_get_intr_mask(guard->dev, &after);
|
||||
if (before & after & I2C_ERROR_INTR)
|
||||
i2c_ll_disable_intr_mask(guard->dev, I2C_LL_MASTER_EVENT_INTR);
|
||||
}
|
||||
|
||||
static int i2c_port_for_source(int source) {
|
||||
for (int port = 0; port < SOC_HP_I2C_NUM; port++) {
|
||||
if (source == i2c_periph_signal[port].irq)
|
||||
return port;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} // namespace esphome::i2c
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
esp_err_t __real_esp_intr_alloc_intrstatus(int source, int flags, uint32_t reg, uint32_t mask, intr_handler_t handler,
|
||||
void *arg, intr_handle_t *ret_handle);
|
||||
|
||||
esp_err_t __wrap_esp_intr_alloc_intrstatus(int source, int flags, uint32_t reg, uint32_t mask, intr_handler_t handler,
|
||||
void *arg, intr_handle_t *ret_handle) {
|
||||
int port = esphome::i2c::i2c_port_for_source(source);
|
||||
if (port < 0)
|
||||
return __real_esp_intr_alloc_intrstatus(source, flags, reg, mask, handler, arg, ret_handle);
|
||||
auto &guard = esphome::i2c::i2c_isr_guards[port];
|
||||
auto previous = guard;
|
||||
guard = {handler, arg, I2C_LL_GET_HW(port)};
|
||||
esp_err_t err =
|
||||
__real_esp_intr_alloc_intrstatus(source, flags, reg, mask, esphome::i2c::i2c_isr_guard, &guard, ret_handle);
|
||||
if (err != ESP_OK)
|
||||
guard = previous; // a failed allocation leaves the live guard alone
|
||||
return err;
|
||||
}
|
||||
} // extern "C"
|
||||
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
#endif // USE_I2C_ISR_GUARD
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "i2c");
|
||||
@@ -116,6 +180,12 @@ void IDFI2CBus::dump_config() {
|
||||
if (this->timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_);
|
||||
}
|
||||
#ifdef USE_I2C_ISR_GUARD
|
||||
// Null when the IDF driver no longer allocates its interrupt through the wrapped call
|
||||
if (i2c_isr_guards[this->port_].handler == nullptr) {
|
||||
ESP_LOGW(TAG, " Interrupt guard inactive");
|
||||
}
|
||||
#endif
|
||||
switch (this->recovery_result_) {
|
||||
case RECOVERY_COMPLETED:
|
||||
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <esp_idf_version.h>
|
||||
#include <driver/i2s_std.h>
|
||||
#include <soc/soc_caps.h>
|
||||
|
||||
namespace esphome::i2s_audio {
|
||||
|
||||
@@ -24,7 +25,7 @@ class I2SAudioBase : public Parented<I2SAudioComponent> {
|
||||
|
||||
protected:
|
||||
i2s_clock_src_t get_clock_source_() const {
|
||||
#ifdef I2S_CLK_SRC_APLL
|
||||
#if SOC_CLK_APLL_SUPPORTED
|
||||
if (this->use_apll_) {
|
||||
return I2S_CLK_SRC_APLL;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ from esphome.const import (
|
||||
CONF_MOVE_THRESHOLD,
|
||||
CONF_STILL_THRESHOLD,
|
||||
CONF_TIMEOUT,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_LIGHTBULB,
|
||||
ICON_MOTION_SENSOR,
|
||||
@@ -39,19 +37,16 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
|
||||
@@ -6,7 +6,6 @@ from esphome.const import (
|
||||
CONF_LIGHT,
|
||||
CONF_MOVING_DISTANCE,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_FLASH,
|
||||
ICON_LIGHTBULB,
|
||||
@@ -55,7 +54,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
|
||||
@@ -8,8 +8,6 @@ from esphome.const import (
|
||||
CONF_MOVE_THRESHOLD,
|
||||
CONF_STILL_THRESHOLD,
|
||||
CONF_TIMEOUT,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_LIGHTBULB,
|
||||
ICON_MOTION_SENSOR,
|
||||
@@ -33,19 +31,16 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
|
||||
@@ -6,14 +6,12 @@ from esphome.const import (
|
||||
CONF_LIGHT,
|
||||
CONF_MOVING_DISTANCE,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_FLASH,
|
||||
ICON_LIGHTBULB,
|
||||
ICON_MOTION_SENSOR,
|
||||
ICON_SIGNAL,
|
||||
UNIT_CENTIMETER,
|
||||
UNIT_EMPTY,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
@@ -39,11 +37,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
|
||||
icon=ICON_LIGHTBULB,
|
||||
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
|
||||
),
|
||||
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
|
||||
@@ -6,7 +6,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_MOVE_THRESHOLD,
|
||||
CONF_STILL_THRESHOLD,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_MOTION_SENSOR,
|
||||
ICON_SCALE,
|
||||
@@ -41,19 +40,16 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Inclusive(CONF_MIN_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema(
|
||||
LD2420MinDistanceNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Inclusive(CONF_MAX_GATE_DISTANCE, TIMEOUT_GROUP): number.number_schema(
|
||||
LD2420MaxDistanceNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Inclusive(CONF_GATE_SELECT, GATE_GROUP): number.number_schema(
|
||||
LD2420GateSelectNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
@@ -69,13 +65,11 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Optional(CONF_GATE_MOVE_SENSITIVITY): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
cv.Optional(CONF_GATE_STILL_SENSITIVITY): number.number_schema(
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_SCALE,
|
||||
),
|
||||
|
||||
@@ -61,6 +61,8 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Fault handlers the crash handler wraps (BK7238, BK7231N)
|
||||
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
|
||||
# Timing functions RTL8710B builds replace (rtl8710b_micros.c)
|
||||
RTL8710B_TIME_WRAPS = ("millis", "micros", "delay", "delayMicroseconds")
|
||||
# Families with a crash handler; the Cortex-M ones install it at run time (no wraps).
|
||||
CRASH_HANDLER_FAMILIES = (
|
||||
FAMILY_BK7231N,
|
||||
@@ -158,6 +160,16 @@ def _update_core_data(config):
|
||||
return config
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Leave the RTL8710B timing wrappers out of every other family's build."""
|
||||
if (data := CORE.data.get(KEY_LIBRETINY)) is None or data.get(KEY_FAMILY) in (
|
||||
None,
|
||||
FAMILY_RTL8710B,
|
||||
):
|
||||
return []
|
||||
return ["rtl8710b_micros.c"]
|
||||
|
||||
|
||||
def get_libretiny_component(core_obj=None):
|
||||
return (core_obj or CORE).data[KEY_LIBRETINY][KEY_COMPONENT]
|
||||
|
||||
@@ -529,6 +541,9 @@ async def component_to_code(config):
|
||||
# RTL8720C (ambz2) requires FreeRTOS 10.x so this only applies to RTL8710B
|
||||
if config[CONF_FAMILY] == FAMILY_RTL8710B:
|
||||
cg.add_platformio_option("custom_versions.freertos", "8.2.3")
|
||||
# millis(), micros() and delays on the system timer and CPU cycles (see rtl8710b_micros.c)
|
||||
for func in RTL8710B_TIME_WRAPS:
|
||||
cg.add_build_flag(f"-Wl,--wrap={func}")
|
||||
|
||||
# LibreTiny bundles BDK 3.0.33 for these two but already builds BK7238 with 3.0.78,
|
||||
# where STA+AP works (esphome#19430), the web server OTA does not trip the watchdog
|
||||
|
||||
@@ -37,6 +37,33 @@ ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const {
|
||||
const_cast<ArduinoInternalGPIOPin *>(this)); // NOLINT(cppcoreguidelines-pro-type-const-cast)
|
||||
}
|
||||
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8710B) && !defined(CLANG_TIDY)
|
||||
// RTL8710B GPIO interrupts on one edge only and LibreTiny maps CHANGE to rising, so flip the edge on every interrupt.
|
||||
// Mirrors LibreTiny 1.14.0 wiring_irq.c (PinData is the SDK handler id); drop once libretiny-eu/libretiny#155 is fixed.
|
||||
// Re-reads the level after each write; a pin still bouncing after four tries is corrected at its next edge.
|
||||
static void IRAM_ATTR rtl8710b_arm_next_edge(gpio_irq_t *irq) {
|
||||
uint32_t level = GPIO_ReadDataBit(irq->pin);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// gpio_irq_set() without its flash wrapper, so the handler runs from RAM and mask ROM only
|
||||
GPIO_INTMode(irq->pin, ENABLE, GPIO_INT_Trigger_EDGE,
|
||||
level ? GPIO_INT_POLARITY_ACTIVE_LOW : GPIO_INT_POLARITY_ACTIVE_HIGH, GPIO_INT_DEBOUNCE_ENABLE);
|
||||
const uint32_t now = GPIO_ReadDataBit(irq->pin);
|
||||
if (now == level)
|
||||
return;
|
||||
level = now;
|
||||
}
|
||||
}
|
||||
|
||||
static void IRAM_ATTR rtl8710b_change_handler(uint32_t id, gpio_irq_event event) {
|
||||
auto *data = reinterpret_cast<PinData *>(id); // NOLINT(performance-no-int-to-ptr)
|
||||
// A later attach with another mode keeps this handler
|
||||
if (data->irqMode == CHANGE)
|
||||
rtl8710b_arm_next_edge(data->irq);
|
||||
if (data->irqHandler != nullptr)
|
||||
reinterpret_cast<void (*)(void *)>(data->irqHandler)(data->irqParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
PinStatus arduino_mode = (PinStatus) 255;
|
||||
switch (type) {
|
||||
@@ -58,6 +85,20 @@ void ArduinoInternalGPIOPin::attach_interrupt_(void (*func)(void *), void *arg,
|
||||
}
|
||||
|
||||
attachInterruptParam(pin_, func, arduino_mode, arg);
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8710B) && !defined(CLANG_TIDY)
|
||||
if (arduino_mode == CHANGE) {
|
||||
PinInfo *info = pinInfo(this->pin_);
|
||||
PinData *data = info != nullptr ? info->data : nullptr;
|
||||
if (data != nullptr && data->irq != nullptr) {
|
||||
// The interrupt is already live: keep it from arming an edge between the level read and the write below
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
GPIO_UserRegIrq(data->irq->pin, reinterpret_cast<void *>(rtl8710b_change_handler), data);
|
||||
rtl8710b_arm_next_edge(data->irq);
|
||||
__set_PRIMASK(primask);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
@@ -120,6 +161,13 @@ size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
}
|
||||
|
||||
bool ArduinoInternalGPIOPin::digital_read() {
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) && !defined(CLANG_TIDY)
|
||||
// The Arduino read switches the pin back to GPIO and frees its interrupt, so read through the interrupt instead.
|
||||
// Remove once LibreTiny's Arduino pin read handles this. The IRQ bit is only set once data->irq exists.
|
||||
PinInfo *info = pinInfo(this->pin_);
|
||||
if (info != nullptr && (info->enabled & PIN_IRQ))
|
||||
return bool(gpio_irq_read(info->data->irq)) ^ this->inverted_;
|
||||
#endif
|
||||
return bool(digitalRead(this->pin_)) ^ this->inverted_; // NOLINT
|
||||
}
|
||||
void ArduinoInternalGPIOPin::digital_write(bool value) {
|
||||
|
||||
@@ -94,7 +94,10 @@ __attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
|
||||
// Per-variant millis() fast path — matches MillisInternal::get().
|
||||
#if defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// The RTL8710B tick is 976.6 us; millis() comes from the system timer instead (rtl8710b_micros.c)
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(::millis()); }
|
||||
#elif defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
|
||||
__attribute__((always_inline)) inline uint32_t millis() {
|
||||
// xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// RTL8710B: the FreeRTOS tick is 1/1024 s, so millis(), micros() and delay() use the 32.768 kHz system timer instead
|
||||
// and delayMicroseconds() the DWT cycle counter. Linked with -Wl,--wrap (RTL8710B_TIME_WRAPS in __init__.py).
|
||||
|
||||
#ifdef USE_LIBRETINY_VARIANT_RTL8710B
|
||||
|
||||
#include <stdint.h>
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
extern uint32_t SystemCoreClock;
|
||||
__attribute__((long_call)) uint32_t SYSTIMER_TickGet(void); // mask ROM, rtl8710b_delay.h
|
||||
|
||||
#define DEMCR (*(volatile uint32_t *) 0xE000EDFC)
|
||||
#define DEMCR_TRCENA (1UL << 24)
|
||||
#define DWT_CTRL (*(volatile uint32_t *) 0xE0001000)
|
||||
#define DWT_CYCCNT (*(volatile uint32_t *) 0xE0001004)
|
||||
// Interrupt handlers call these, so they live in RAM like IRAM_ATTR (hal.h)
|
||||
#define RTL8710B_RAM_TEXT __attribute__((noinline, section(".sram.text")))
|
||||
|
||||
// Extends the 32-bit timer, which wraps every 36.4 hours; the main loop calls millis() far more often
|
||||
static inline __attribute__((always_inline)) uint64_t timer_ticks(void) {
|
||||
static uint64_t ticks;
|
||||
uint32_t primask;
|
||||
__asm volatile("mrs %0, primask\n cpsid i" : "=r"(primask)::"memory");
|
||||
ticks += (uint32_t) (SYSTIMER_TickGet() - (uint32_t) ticks);
|
||||
const uint64_t now = ticks;
|
||||
__asm volatile("msr primask, %0" ::"r"(primask) : "memory");
|
||||
return now;
|
||||
}
|
||||
|
||||
RTL8710B_RAM_TEXT unsigned long __wrap_micros(void) { // NOLINT(readability-identifier-naming)
|
||||
// 1000000 / 32768 = 15625 / 512
|
||||
return (uint32_t) ((timer_ticks() * 15625) >> 9);
|
||||
}
|
||||
|
||||
RTL8710B_RAM_TEXT unsigned long __wrap_millis(void) { // NOLINT(readability-identifier-naming)
|
||||
// 1000 / 32768 = 125 / 4096
|
||||
return (uint32_t) ((timer_ticks() * 125) >> 12);
|
||||
}
|
||||
|
||||
void __wrap_delay(uint32_t ms) { // NOLINT(readability-identifier-naming)
|
||||
if (ms == 0) {
|
||||
vTaskDelay(0);
|
||||
return;
|
||||
}
|
||||
// vTaskDelay() counts the partial tick, so sleep to a deadline; +1 covers the elapsed part of the current count
|
||||
const uint64_t deadline = timer_ticks() + ((uint64_t) ms * 32768 + 999) / 1000 + 1;
|
||||
// Sleeps are capped at 1024 s so a delay longer than the 36.4 hour timer wrap still sees every wrap
|
||||
for (uint64_t now = timer_ticks(); now < deadline; now = timer_ticks()) {
|
||||
const uint64_t ticks = (deadline - now + 31) / 32;
|
||||
vTaskDelay((TickType_t) (ticks < 0x100000 ? ticks : 0x100000));
|
||||
}
|
||||
}
|
||||
|
||||
RTL8710B_RAM_TEXT void __wrap_delayMicroseconds(unsigned int us) { // NOLINT(readability-identifier-naming)
|
||||
DEMCR |= DEMCR_TRCENA;
|
||||
DWT_CTRL |= 1;
|
||||
uint32_t start = DWT_CYCCNT;
|
||||
// Rounded up: the BW12 runs at 62.5 MHz
|
||||
uint64_t cycles = (uint64_t) us * ((SystemCoreClock + 999999) / 1000000);
|
||||
// Steps of at most 2^31 cycles keep the 32-bit difference from wrapping
|
||||
while (cycles > 0) {
|
||||
const uint32_t step = cycles > 0x80000000UL ? 0x80000000UL : (uint32_t) cycles;
|
||||
while (DWT_CYCCNT - start < step) {
|
||||
}
|
||||
start += step;
|
||||
cycles -= step;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8710B
|
||||
@@ -88,6 +88,7 @@ from .types import ( # noqa: F401
|
||||
LightStateTrigger,
|
||||
LightTurnOffTrigger,
|
||||
LightTurnOnTrigger,
|
||||
PowerEstimation,
|
||||
light_ns,
|
||||
)
|
||||
|
||||
@@ -702,6 +703,7 @@ async def setup_light_core_(light_var, config, output_var):
|
||||
cg.add_define("USE_LIGHT_POWER_ESTIMATION")
|
||||
cg.add(
|
||||
output_var.set_power_estimation(
|
||||
cg.static_buffer(config[CONF_OUTPUT_ID], PowerEstimation, 1),
|
||||
power_estimation[CONF_MA_PER_LED_RED],
|
||||
power_estimation[CONF_MA_PER_LED_GREEN],
|
||||
power_estimation[CONF_MA_PER_LED_BLUE],
|
||||
|
||||
@@ -34,7 +34,7 @@ class AddressableLightState final : public LightState {
|
||||
};
|
||||
|
||||
#ifdef USE_LIGHT_POWER_ESTIMATION
|
||||
// Allocated only for a strip with power_estimation configured
|
||||
// Static storage from codegen, only for a strip with power_estimation configured
|
||||
struct PowerEstimation {
|
||||
float ma_per_level[4];
|
||||
float idle_ma_per_led;
|
||||
@@ -87,15 +87,14 @@ class AddressableLight : public LightOutput, public Component {
|
||||
}
|
||||
#ifdef USE_LIGHT_POWER_ESTIMATION
|
||||
// Per-channel draw at full level and idle draw per LED
|
||||
void set_power_estimation(float ma_red, float ma_green, float ma_blue, float ma_white, float idle_ma_per_led) {
|
||||
if (this->power_estimation_ == nullptr)
|
||||
this->power_estimation_ = new PowerEstimation(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
auto *power = this->power_estimation_;
|
||||
power->ma_per_level[0] = ma_red / 255.0f;
|
||||
power->ma_per_level[1] = ma_green / 255.0f;
|
||||
power->ma_per_level[2] = ma_blue / 255.0f;
|
||||
power->ma_per_level[3] = ma_white / 255.0f;
|
||||
power->idle_ma_per_led = idle_ma_per_led;
|
||||
void set_power_estimation(PowerEstimation *storage, float ma_red, float ma_green, float ma_blue, float ma_white,
|
||||
float idle_ma_per_led) {
|
||||
this->power_estimation_ = storage;
|
||||
storage->ma_per_level[0] = ma_red / 255.0f;
|
||||
storage->ma_per_level[1] = ma_green / 255.0f;
|
||||
storage->ma_per_level[2] = ma_blue / 255.0f;
|
||||
storage->ma_per_level[3] = ma_white / 255.0f;
|
||||
storage->idle_ma_per_led = idle_ma_per_led;
|
||||
}
|
||||
// Called after set_power_estimation()
|
||||
void set_max_current_ma(float max_current_ma) { this->power_estimation_->max_current_ma = max_current_ma; }
|
||||
|
||||
@@ -18,6 +18,7 @@ LightStateRTCState = light_ns.struct("LightStateRTCState")
|
||||
|
||||
# Addressable strips
|
||||
ChannelColors = light_ns.struct("ChannelColors")
|
||||
PowerEstimation = light_ns.struct("PowerEstimation")
|
||||
|
||||
# Color modes
|
||||
ColorMode = light_ns.enum("ColorMode", is_class=True)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_SPEED,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_METER,
|
||||
@@ -40,11 +39,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_SPATIAL_STATIC_VALUE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
icon="mdi:counter",
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_SPATIAL_MOTION_VALUE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
icon="mdi:counter",
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_MOTION_SPEED): sensor.sensor_schema(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -112,7 +112,6 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_ACTUAL_GAIN): sensor.sensor_schema(
|
||||
icon=ICON_BRIGHTNESS_6,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "vl53l0x_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
/*
|
||||
@@ -268,8 +269,16 @@ void VL53L0XSensor::update() {
|
||||
this->status_momentary_warning(5000);
|
||||
ESP_LOGW(TAG, "%s - update called before prior reading complete - initiated:%d waiting_for_interrupt:%d",
|
||||
LOG_STR_ARG(this->get_log_name()), this->initiated_read_, this->waiting_for_interrupt_);
|
||||
return;
|
||||
// A measurement gets its timing budget plus one more interval; one still pending after that is stalled
|
||||
const uint32_t interval_ms = std::max<uint32_t>(this->get_update_interval(), 1);
|
||||
const uint32_t grace = std::min<uint32_t>(this->measurement_timing_budget_us_ / 1000 / interval_ms + 1, UINT8_MAX);
|
||||
if (this->pending_updates_ < grace) {
|
||||
this->pending_updates_++;
|
||||
return;
|
||||
}
|
||||
reg(0x0B) = 0x01;
|
||||
}
|
||||
this->pending_updates_ = 0;
|
||||
|
||||
// initiate single shot measurement
|
||||
reg(0x80) = 0x01;
|
||||
@@ -288,10 +297,12 @@ void VL53L0XSensor::update() {
|
||||
}
|
||||
|
||||
void VL53L0XSensor::loop() {
|
||||
// A failed read must not advance the state: it reads as 0, which looks like a finished measurement
|
||||
uint8_t value;
|
||||
if (this->initiated_read_) {
|
||||
if (reg(0x00).get() & 0x01) {
|
||||
// waiting
|
||||
} else {
|
||||
if (!this->read_byte(0x00, &value))
|
||||
return;
|
||||
if (!(value & 0x01)) {
|
||||
// done
|
||||
// wait until reg(0x13) & 0x07 is set
|
||||
this->initiated_read_ = false;
|
||||
@@ -299,11 +310,18 @@ void VL53L0XSensor::loop() {
|
||||
}
|
||||
}
|
||||
if (this->waiting_for_interrupt_) {
|
||||
if (reg(0x13).get() & 0x07) {
|
||||
if (!this->read_byte(0x13, &value))
|
||||
return;
|
||||
if (value & 0x07) {
|
||||
uint16_t range_mm = 0;
|
||||
this->read_byte_16(0x14 + 10, &range_mm);
|
||||
const bool ok = this->read_byte_16(0x14 + 10, &range_mm);
|
||||
reg(0x0B) = 0x01;
|
||||
this->waiting_for_interrupt_ = false;
|
||||
if (!ok) {
|
||||
this->publish_state(NAN);
|
||||
this->status_momentary_warning(5000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (range_mm >= 8190) {
|
||||
ESP_LOGD(TAG, "'%s' - Distance is out of range, please move the target closer",
|
||||
|
||||
@@ -61,7 +61,9 @@ class VL53L0XSensor final : public sensor::Sensor, public PollingComponent, publ
|
||||
uint32_t measurement_timing_budget_us_{0};
|
||||
bool initiated_read_{false};
|
||||
bool waiting_for_interrupt_{false};
|
||||
uint8_t stop_variable_;
|
||||
uint8_t stop_variable_{0};
|
||||
// Updates that found the measurement still pending; past the grace period the next one restarts it
|
||||
uint8_t pending_updates_{0};
|
||||
|
||||
uint32_t timeout_us_{};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,12 +41,14 @@
|
||||
#include "lwip/apps/sntp.h"
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
#ifdef USE_WIFI_LEASE_CACHE
|
||||
#if USE_NETWORK_IPV6 || defined(USE_WIFI_LEASE_CACHE)
|
||||
#include <esp_netif_net_stack.h> // esp_netif_get_netif_impl()
|
||||
#include <esp_rtc_time.h> // esp_rtc_get_time_us() -- RTC-domain clock for lease age
|
||||
#include "lwip/dhcp.h"
|
||||
#include "lwip/netif.h"
|
||||
#endif
|
||||
#ifdef USE_WIFI_LEASE_CACHE
|
||||
#include <esp_rtc_time.h> // esp_rtc_get_time_us() -- RTC-domain clock for lease age
|
||||
#include "lwip/dhcp.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -1098,8 +1100,23 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
s_sta_connected = true;
|
||||
#if USE_NETWORK_IPV6
|
||||
// Start SLAAC on association, not after a DHCPv4 lease that may never arrive.
|
||||
if (esp_err_t ll_err = esp_netif_create_ip6_linklocal(s_sta_netif); ll_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(ll_err));
|
||||
// Not esp_netif_create_ip6_linklocal(): it returns ESP_FAIL while the netif is down, and this queued event
|
||||
// can be handled before esp-netif's own STA_CONNECTED handler brings the netif up. lwIP accepts the
|
||||
// address on a down netif and starts DAD and router solicitation once it comes up.
|
||||
if (auto *netif = static_cast<struct netif *>(esp_netif_get_netif_impl(s_sta_netif)); netif != nullptr) {
|
||||
if (esp_err_t err = esp_netif_tcpip_exec(
|
||||
[](void *ctx) -> esp_err_t {
|
||||
netif_create_ip6_linklocal_address(static_cast<struct netif *>(ctx), 1);
|
||||
return ESP_OK;
|
||||
},
|
||||
netif);
|
||||
err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "IPv6 link-local not created: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "IPv6 link-local address created");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No lwIP netif, IPv6 link-local not created");
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.11.0b2"
|
||||
__version__ = "2026.11.0b4"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -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
|
||||
@@ -698,6 +701,9 @@
|
||||
#define USE_UDP_OMR_SOURCE
|
||||
#define USE_ZIGBEE
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32)
|
||||
#define USE_I2C_ISR_GUARD
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
// Border router with a UART RCP; a native-radio border router (C5/C6) adds only the coexistence call
|
||||
#define USE_OPENTHREAD
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -35,6 +35,9 @@ class MillisInternal {
|
||||
static ESPHOME_ALWAYS_INLINE uint32_t get() {
|
||||
#if defined(USE_ESP32) && CONFIG_FREERTOS_HZ == 1000
|
||||
return xTaskGetTickCount();
|
||||
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// The tick is 976.6 us here, so millis() reads the system timer (rtl8710b_micros.c)
|
||||
return millis();
|
||||
#elif defined(USE_LIBRETINY) && (defined(USE_RTL87XX) || defined(USE_LN882X))
|
||||
// 1 kHz: xTaskGetTickCount() is already ms.
|
||||
static_assert(configTICK_RATE_HZ == 1000, "MillisInternal fast path requires 1 kHz FreeRTOS tick");
|
||||
|
||||
@@ -567,6 +567,22 @@ def shared_progmem_string(value: str) -> Expression:
|
||||
return text
|
||||
|
||||
|
||||
def static_buffer(owner: ID, type_: "MockObjClass", size: int | str) -> "MockObj":
|
||||
"""Emit a zeroed file-scope ``static type_ <component>__<owner>_buffer__pstorage[size];``.
|
||||
|
||||
Named like placement new storage, so ``esphome analyze-memory`` counts it under the owner.
|
||||
``size`` may be a C++ constant expression.
|
||||
"""
|
||||
if not isinstance(owner, ID) or owner.type is None:
|
||||
raise EsphomeError(f"Buffer owner '{owner}' must be the owning component's ID")
|
||||
base = unique_global_id(f"{owner.id}_buffer", type_)
|
||||
name = _pstorage_name(owner.type, base)
|
||||
CORE.add_global(RawStatement(f"static {type_} {name}[{size}];"))
|
||||
buffer = MockObj(name, ".")
|
||||
CORE.register_variable(base, buffer)
|
||||
return buffer
|
||||
|
||||
|
||||
def static_const_array(id_, rhs) -> "MockObj":
|
||||
rhs = safe_exp(rhs)
|
||||
obj = MockObj(id_, ".")
|
||||
@@ -669,6 +685,11 @@ def new_variable(
|
||||
return obj
|
||||
|
||||
|
||||
def _pstorage_name(type_: Any, name: Any) -> str:
|
||||
"""``<component>__<name>__pstorage``, the static storage name ``esphome analyze-memory`` attributes."""
|
||||
return f"{_extract_component_ns(str(type_))}__{name}__pstorage"
|
||||
|
||||
|
||||
def _extract_component_ns(type_str: str) -> str:
|
||||
"""Extract the component namespace from a fully-qualified C++ type string.
|
||||
|
||||
@@ -726,9 +747,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
||||
if call_expr.template_args is not None:
|
||||
actual_type = f"{actual_type}{call_expr.template_args}"
|
||||
pointer_type = id_.type
|
||||
# Extract component namespace from type for memory analysis attribution
|
||||
component_ns = _extract_component_ns(str(actual_type))
|
||||
storage_name = f"{component_ns}__{id_.id}__pstorage"
|
||||
storage_name = _pstorage_name(actual_type, id_.id)
|
||||
|
||||
# Declare aligned byte array for the object storage
|
||||
CORE.add_global(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -212,6 +212,8 @@ def clang_options(idedata, environment):
|
||||
and not flag.startswith("-mtune=esp")
|
||||
)
|
||||
cmd.append("-std=gnu++20")
|
||||
# clang otherwise reads the `module:` bit-field in IDF's soc/regdma.h as a C++20 module directive
|
||||
cmd.append("-fno-cxx-modules")
|
||||
|
||||
if environment.startswith(("bk72xx", "ln882h", "rtl87xx")):
|
||||
# LibreTiny leaves function-like macro values unparenthesized
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: isr-guard-esp32
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: isr-guard-esp32c3
|
||||
|
||||
esp32:
|
||||
board: esp32-c3-devkitm-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
i2c:
|
||||
sda: GPIO5
|
||||
scl: GPIO6
|
||||
@@ -0,0 +1,31 @@
|
||||
"""The I2C ISR guard define and its linker wrap are emitted together, only for classic ESP32."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
# Spelled out so a typo in the component's flag fails here
|
||||
_WRAP_FLAG = "-Wl,--wrap=esp_intr_alloc_intrstatus"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("isr_guard_esp32.yaml", True, id="esp32"),
|
||||
pytest.param("isr_guard_esp32c3.yaml", False, id="esp32c3"),
|
||||
],
|
||||
)
|
||||
def test_guard_define_and_wrap_flag_go_together(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
generate_main(component_config_path(config_file))
|
||||
defines = {define.name for define in CORE.defines}
|
||||
# A define without the flag leaves the guard uninstalled; a flag without the define fails to link
|
||||
assert ("USE_I2C_ISR_GUARD" in defines) is expected
|
||||
assert (_WRAP_FLAG in CORE.build_flags) is expected
|
||||
@@ -0,0 +1,42 @@
|
||||
"""RTL8710B builds wrap the timing functions that rtl8710b_micros.c replaces and compile that file; other families
|
||||
get neither, since a flag without the wrappers fails the link."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.libretiny import FILTER_SOURCE_FILES, RTL8710B_TIME_WRAPS
|
||||
from esphome.components.libretiny.const import KEY_LIBRETINY
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "wrapped"),
|
||||
[
|
||||
("rtl8710b.yaml", True),
|
||||
("rtl8720c.yaml", False),
|
||||
("ln882x.yaml", False),
|
||||
("bk72xx.yaml", False),
|
||||
("bk7238.yaml", False),
|
||||
],
|
||||
)
|
||||
def test_rtl8710b_time_wraps(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config: str,
|
||||
wrapped: bool,
|
||||
) -> None:
|
||||
generate_main(component_config_path(config))
|
||||
for func in RTL8710B_TIME_WRAPS:
|
||||
assert (f"-Wl,--wrap={func}" in CORE.build_flags) is wrapped
|
||||
assert ("rtl8710b_micros.c" not in FILTER_SOURCE_FILES()) is wrapped
|
||||
|
||||
|
||||
@pytest.mark.parametrize("core_data", [{}, {KEY_LIBRETINY: {}}])
|
||||
def test_nothing_excluded_before_the_family_is_known(core_data: dict) -> None:
|
||||
with patch("esphome.components.libretiny.CORE.data", core_data):
|
||||
assert FILTER_SOURCE_FILES() == []
|
||||
@@ -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!"
|
||||
@@ -1,5 +1,6 @@
|
||||
display:
|
||||
- platform: ht16k33_char
|
||||
i2c_id: i2c_bus
|
||||
device: ADAFRUIT_7_SEG_.56IN
|
||||
address: 0x70
|
||||
intensity: 10
|
||||
@@ -15,6 +16,7 @@ display:
|
||||
it.printf(5, "%d°", 21);
|
||||
# No add_characters, beside one that has them
|
||||
- platform: ht16k33_char
|
||||
i2c_id: i2c_bus
|
||||
device: ADAFRUIT_7_SEG_.56IN
|
||||
address: 0x72
|
||||
lambda: |-
|
||||
|
||||
@@ -34,7 +34,6 @@ class Strip : public AddressableLight {
|
||||
this->accessor_ = &this->internal_accessor_;
|
||||
}
|
||||
|
||||
~Strip() { delete this->power_estimation_; } // NOLINT(cppcoreguidelines-owning-memory)
|
||||
int32_t size() const override { return this->num_leds_; }
|
||||
void clear_effect_data() override {}
|
||||
LightTraits get_traits() override { return {}; }
|
||||
@@ -42,7 +41,8 @@ class Strip : public AddressableLight {
|
||||
uint8_t sent_red{0};
|
||||
// Every channel draws ma_per_channel at full level; setup turns the settings into the idle draw and budget
|
||||
void configure_power(float ma_per_channel, float idle_ma_per_led, float max_current_ma) {
|
||||
this->set_power_estimation(ma_per_channel, ma_per_channel, ma_per_channel, ma_per_channel, idle_ma_per_led);
|
||||
this->set_power_estimation(&this->power_storage_, ma_per_channel, ma_per_channel, ma_per_channel, ma_per_channel,
|
||||
idle_ma_per_led);
|
||||
this->set_max_current_ma(max_current_ma);
|
||||
this->call_setup();
|
||||
}
|
||||
@@ -51,6 +51,7 @@ class Strip : public AddressableLight {
|
||||
uint8_t power_brightness() const { return this->correction_.get_power_brightness(); }
|
||||
|
||||
protected:
|
||||
PowerEstimation power_storage_;
|
||||
const size_t num_leds_;
|
||||
std::vector<uint8_t> buffer_;
|
||||
light::CorrectedBufferColorAccessor internal_accessor_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The driver lives in the sensor platform, so pull in what it builds against.
|
||||
manifest.dependencies = manifest.dependencies + ["i2c", "sensor"]
|
||||
@@ -0,0 +1,173 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/vl53l0x/vl53l0x_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::vl53l0x::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x29;
|
||||
static constexpr uint8_t SYSRANGE_START = 0x00;
|
||||
static constexpr uint8_t SYSTEM_INTERRUPT_CLEAR = 0x0B;
|
||||
static constexpr uint8_t RESULT_INTERRUPT_STATUS = 0x13;
|
||||
static constexpr uint8_t RESULT_RANGE_MM = 0x14 + 10;
|
||||
|
||||
// The sensor's registers behind an address pointer that moves on after every byte. Reads of one
|
||||
// register can be made to fail, and every write is recorded.
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (size != 0 && out_size != 0 && data[0] == this->fail_reg && this->fail_count > 0) {
|
||||
this->fail_count--;
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
if (size != 0) {
|
||||
this->pointer_ = data[0];
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
this->writes.emplace_back(this->pointer_, data[i]);
|
||||
this->registers[this->pointer_++] = data[i];
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < out_size; i++)
|
||||
out[i] = this->registers[this->pointer_++];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
bool wrote(uint8_t reg, uint8_t value) const {
|
||||
for (const auto &write : this->writes) {
|
||||
if (write.first == reg && write.second == value)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ranging done (start bit clear), interrupt raised and a range of 300 mm
|
||||
void finish_measurement() {
|
||||
this->registers[SYSRANGE_START] = 0x00;
|
||||
this->registers[RESULT_INTERRUPT_STATUS] = 0x07;
|
||||
this->registers[RESULT_RANGE_MM] = 0x01;
|
||||
this->registers[RESULT_RANGE_MM + 1] = 0x2C;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 256> registers{};
|
||||
std::vector<std::pair<uint8_t, uint8_t>> writes;
|
||||
int fail_reg{-1};
|
||||
int fail_count{0};
|
||||
|
||||
protected:
|
||||
uint8_t pointer_{0};
|
||||
};
|
||||
|
||||
class VL53L0XRecoveryTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->sensor_.set_i2c_bus(&this->bus_);
|
||||
this->sensor_.set_i2c_address(ADDRESS);
|
||||
}
|
||||
// A momentary warning leaves a timeout on the shared scheduler that would outlive the sensor
|
||||
void TearDown() override { App.scheduler.cancel_timeout(&this->sensor_, InternalSchedulerID::STATUS_WARNING); }
|
||||
|
||||
RegisterBus bus_;
|
||||
VL53L0XSensor sensor_;
|
||||
};
|
||||
|
||||
TEST_F(VL53L0XRecoveryTest, FailedStatusReadDoesNotFinishMeasurement) {
|
||||
this->sensor_.update();
|
||||
// Still ranging, with a stale result left in the result registers, and the status read fails. A failed
|
||||
// read used to read as 0, "done", so the stale result was published.
|
||||
this->bus_.registers[RESULT_INTERRUPT_STATUS] = 0x07;
|
||||
this->bus_.registers[RESULT_RANGE_MM] = 0x00;
|
||||
this->bus_.registers[RESULT_RANGE_MM + 1] = 0x64;
|
||||
this->bus_.fail_reg = SYSRANGE_START;
|
||||
this->bus_.fail_count = 1;
|
||||
this->bus_.writes.clear();
|
||||
this->sensor_.loop();
|
||||
EXPECT_TRUE(this->bus_.writes.empty());
|
||||
EXPECT_FALSE(this->sensor_.has_state());
|
||||
|
||||
this->bus_.finish_measurement();
|
||||
this->sensor_.loop();
|
||||
ASSERT_TRUE(this->sensor_.has_state());
|
||||
EXPECT_FLOAT_EQ(this->sensor_.state, 0.3f);
|
||||
}
|
||||
|
||||
TEST_F(VL53L0XRecoveryTest, StalledMeasurementRestartsAtSecondUpdate) {
|
||||
this->sensor_.update();
|
||||
this->sensor_.loop(); // still ranging
|
||||
|
||||
this->bus_.writes.clear();
|
||||
this->sensor_.update();
|
||||
EXPECT_TRUE(std::isnan(this->sensor_.state));
|
||||
EXPECT_TRUE(this->bus_.writes.empty()) << "a measurement still pending at one update gets another interval";
|
||||
|
||||
this->sensor_.update();
|
||||
EXPECT_TRUE(this->bus_.wrote(SYSTEM_INTERRUPT_CLEAR, 0x01));
|
||||
EXPECT_TRUE(this->bus_.wrote(SYSRANGE_START, 0x01));
|
||||
|
||||
this->bus_.finish_measurement();
|
||||
this->sensor_.loop();
|
||||
EXPECT_FLOAT_EQ(this->sensor_.state, 0.3f);
|
||||
}
|
||||
|
||||
TEST_F(VL53L0XRecoveryTest, SlowMeasurementCompletesWithoutRestart) {
|
||||
this->sensor_.update();
|
||||
this->sensor_.update(); // pending: wait one more interval
|
||||
|
||||
this->bus_.finish_measurement();
|
||||
this->sensor_.loop();
|
||||
EXPECT_FLOAT_EQ(this->sensor_.state, 0.3f);
|
||||
|
||||
// The next update starts a fresh measurement without treating the last one as stalled
|
||||
this->bus_.writes.clear();
|
||||
this->sensor_.update();
|
||||
EXPECT_FALSE(this->bus_.wrote(SYSTEM_INTERRUPT_CLEAR, 0x01));
|
||||
EXPECT_TRUE(this->bus_.wrote(SYSRANGE_START, 0x01));
|
||||
}
|
||||
|
||||
TEST_F(VL53L0XRecoveryTest, LongTimingBudgetGetsMoreIntervals) {
|
||||
// 300 ms of timing budget at 100 ms per update: the measurement gets four updates before a restart
|
||||
this->sensor_.set_update_interval(100);
|
||||
this->sensor_.set_timing_budget(300000);
|
||||
this->sensor_.update();
|
||||
this->bus_.writes.clear();
|
||||
for (int i = 0; i < 4; i++)
|
||||
this->sensor_.update();
|
||||
EXPECT_FALSE(this->bus_.wrote(SYSTEM_INTERRUPT_CLEAR, 0x01));
|
||||
|
||||
this->bus_.finish_measurement();
|
||||
this->sensor_.loop();
|
||||
EXPECT_FLOAT_EQ(this->sensor_.state, 0.3f);
|
||||
|
||||
// Still pending at the fifth update: stalled, so it restarts
|
||||
this->sensor_.update();
|
||||
this->bus_.registers[SYSRANGE_START] = 0x01;
|
||||
this->bus_.writes.clear();
|
||||
for (int i = 0; i < 5; i++)
|
||||
this->sensor_.update();
|
||||
EXPECT_TRUE(this->bus_.wrote(SYSTEM_INTERRUPT_CLEAR, 0x01));
|
||||
}
|
||||
|
||||
TEST_F(VL53L0XRecoveryTest, FailedResultReadPublishesNanAndClearsInterrupt) {
|
||||
this->sensor_.update();
|
||||
this->bus_.finish_measurement();
|
||||
this->bus_.fail_reg = RESULT_RANGE_MM;
|
||||
this->bus_.fail_count = 1;
|
||||
this->bus_.writes.clear();
|
||||
this->sensor_.loop();
|
||||
ASSERT_TRUE(this->sensor_.has_state());
|
||||
EXPECT_TRUE(std::isnan(this->sensor_.state));
|
||||
EXPECT_TRUE(this->bus_.wrote(SYSTEM_INTERRUPT_CLEAR, 0x01));
|
||||
|
||||
// Nothing is left pending, so the next update starts a measurement right away
|
||||
this->bus_.writes.clear();
|
||||
this->sensor_.update();
|
||||
EXPECT_TRUE(this->bus_.wrote(SYSRANGE_START, 0x01));
|
||||
}
|
||||
|
||||
} // namespace esphome::vl53l0x::testing
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -941,6 +941,29 @@ class TestSharedProgmemArray:
|
||||
assert str(array) == "table_2"
|
||||
|
||||
|
||||
class TestStaticBuffer:
|
||||
def test_is_named_after_its_owner_and_unique(self) -> None:
|
||||
hub = ID(
|
||||
"hub",
|
||||
is_declaration=True,
|
||||
type=ct.esphome_ns.namespace("uart").class_("Hub"),
|
||||
)
|
||||
CORE.config = {"sensor": [{"id": ID("hub_buffer", is_declaration=True)}]}
|
||||
cg.static_buffer(hub, ct.uint8, 64)
|
||||
b = cg.static_buffer(hub, ct.uint8, 8)
|
||||
assert [str(st) for st in CORE.global_statements] == [
|
||||
"static uint8_t uart__hub_buffer_2__pstorage[64];",
|
||||
"static uint8_t uart__hub_buffer_3__pstorage[8];",
|
||||
]
|
||||
assert str(b) == "uart__hub_buffer_3__pstorage"
|
||||
|
||||
def test_owner_must_be_an_id(self) -> None:
|
||||
with pytest.raises(EsphomeError, match="owning component's ID"):
|
||||
cg.static_buffer("hub", ct.uint8, 8)
|
||||
with pytest.raises(EsphomeError, match="owning component's ID"):
|
||||
cg.static_buffer(ID("hub"), ct.uint8, 8)
|
||||
|
||||
|
||||
class TestProgmemString:
|
||||
@staticmethod
|
||||
def _set_platform(platform: str) -> None:
|
||||
|
||||
Reference in new issue
Block a user