1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-10 11:55:52 +00:00

Compare commits

..

37 Commits

Author SHA1 Message Date
J. Nick Koston
347501d895 wifi fixed vector 2025-10-12 19:39:55 -10:00
J. Nick Koston
4c00861760 add comments for bot 2025-10-11 17:35:31 -10:00
J. Nick Koston
2ff3e7fb2b add comments for bot 2025-10-11 17:34:51 -10:00
J. Nick Koston
b0c20d7adb [core] Optimize looping_components_ with FixedVector to save flash 2025-10-11 16:54:40 -10:00
Jonathan Swoboda
2cc5e24b38 [esp32] Change Arduino dev & latest to 3.3.2 (#11169) 2025-10-11 20:44:44 -04:00
J. Nick Koston
3afa73b449 [ci] Filter out components without tests from CI test jobs (#11134 followup) (#11178) 2025-10-11 18:27:18 -05:00
J. Nick Koston
dcf2697a2a Group component tests to reduce CI time (#11134) 2025-10-12 07:21:45 +13:00
J. Nick Koston
6a11700a6b [mdns] Restore mdns_txt_record() public API for external components (#11158) 2025-10-12 07:21:37 +13:00
J. Nick Koston
9bd9b043c8 [esp32_ble_tracker] Replace std::vector with StaticVector for listeners and clients (#11173) 2025-10-11 05:47:42 -10:00
J. Nick Koston
cb602c9b1a [esp32_ble] Partial revert of #10862 - Fix GATT client notifications (#11171) 2025-10-11 05:47:23 -10:00
dependabot[bot]
b54beb357a Bump github/codeql-action from 4.30.7 to 4.30.8 (#11163)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-10 13:46:27 -10:00
J. Nick Koston
6abc2efd96 [json] Fix PSRAM allocator dangling pointer crash (#11165) 2025-10-10 21:18:57 +00:00
J. Nick Koston
be51093a7e [ci][tests] Remove all redundant ESP32-C3 Arduino tests (#11154) 2025-10-10 16:02:18 +13:00
J. Nick Koston
52219c4dcc [datetime][ci][tests] Replace test.all.yaml with minimal platform cover (#11151) 2025-10-09 13:45:59 -10:00
J. Nick Koston
590cae13c0 [ci][tests] Remove redundant ESP32-C3 Arduino tests for non-variant-specific components (#11152) 2025-10-09 18:41:50 -05:00
J. Nick Koston
e15429b0f5 [opentherm][ci][tests] Remove redundant ESP32 Arduino tests and simplify conditionals (#11149) 2025-10-09 23:38:34 +00:00
J. Nick Koston
b5cc668a45 [ci][logger][tests] Remove redundant ESP32 Arduino test files (#11144) 2025-10-09 13:30:05 -10:00
Jonathan Swoboda
a1b0ae78e0 [stale] Increase operations-per-run (#11135)
CI passed, stuck on status
2025-10-09 19:10:09 -04:00
J. Nick Koston
fcc8a809e6 [ci][debug][tests] Remove redundant ESP32 variant Arduino test files (#11146) 2025-10-09 16:57:40 -05:00
J. Nick Koston
48474c0f8c [ci][time][tests] Remove redundant ESP32 Arduino test files (#11147) 2025-10-09 16:57:11 -05:00
J. Nick Koston
9f9c95dd09 [network][ci][tests] Remove redundant ESP32 Arduino test files (#11148) 2025-10-09 16:56:53 -05:00
J. Nick Koston
a74fcbc8b6 [esp32_ble_beacon, esp32_ble_tracker] Remove unused Arduino includes and redundant tests (#11140) 2025-10-09 11:42:25 -10:00
J. Nick Koston
c8b898f9c5 [ci][mdns][tests] Remove redundant ESP32 Arduino test files (#11143) 2025-10-09 11:40:47 -10:00
J. Nick Koston
81bf2688b4 [esp32] Update migration warning for Arduino-as-IDF-component transition (#11142) 2025-10-09 11:36:31 -10:00
Jonathan Swoboda
87d2c9868f [esp32] Update IDF 5.5 and Arduino 3.3 to use 55.03.31-1 (#11120) 2025-10-09 21:27:36 +00:00
J. Nick Koston
5ca407e27c [mdns] Store TXT record values in flash to reduce heap usage (#11114) 2025-10-10 09:01:58 +13:00
dependabot[bot]
5bbc2ab482 Bump pyupgrade from 3.20.0 to 3.21.0 (#11139)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 19:40:40 +00:00
J. Nick Koston
309e8b4c92 [ci][improv_serial][tests] Remove redundant ESP32 Arduino test files (#11138) 2025-10-09 19:17:04 +00:00
Jesse Hills
eee2987c99 Merge branch 'beta' into dev 2025-10-10 07:53:53 +13:00
J. Nick Koston
061e55f8c5 [ci][ethernet][tests] Remove redundant Arduino tests for ethernet PHYs (#11137) 2025-10-09 08:45:45 -10:00
J. Nick Koston
56334b7832 [ci][tests] Remove redundant ESP32 Arduino test files (#11136) 2025-10-10 07:26:41 +13:00
J. Nick Koston
a4b7e0c700 [canbus][mcp23xxx_base] Mark virtual methods as pure virtual to fix linker errors (#11133) 2025-10-09 07:41:49 -10:00
Jeff Brown
84ad7ee0e4 [esp32] Accept more framework URL schemes as sources (#11125) 2025-10-09 13:10:48 -04:00
dependabot[bot]
d006008539 Bump esphome-dashboard from 20250904.0 to 20251009.0 (#11123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 09:26:38 -04:00
J. Nick Koston
6bb1e4c9c0 [ci] Reduce component test group size to 10 to prevent runner disk exhaustion (#11122) 2025-10-09 10:35:52 +13:00
J. Nick Koston
82bdb08884 [ci] Reduce component test group size to prevent runner disk exhaustion (#11121) 2025-10-08 14:24:26 -04:00
Jesse Hills
b709ff84c3 Bump version to 2025.11.0-dev 2025-10-08 21:14:45 +13:00
41 changed files with 333 additions and 276 deletions

View File

@@ -433,7 +433,7 @@ jobs:
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
strategy:
fail-fast: false
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
max-parallel: 5
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps:

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
category: "/language:${{matrix.language}}"

View File

@@ -23,7 +23,7 @@ jobs:
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true
operations-per-run: 150
operations-per-run: 400
# The 90 day stale policy for PRs
# - PRs

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.10.0
PROJECT_NUMBER = 2025.11.0-dev
# 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

View File

@@ -268,10 +268,8 @@ def has_ip_address() -> bool:
def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
return CORE.address is not None
"""Check if CORE.address is resolvable (via mDNS or is an IP address)."""
return has_mdns() or has_ip_address()
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
@@ -580,12 +578,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
if has_api():
addresses_to_use: list[str] | None = None
if port_type == "NETWORK":
# Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used
# The resolve_ip_address() function in helpers.py handles all types
if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
addresses_to_use = devices
elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Use MQTT IP lookup for MQTT/MQTTIP types
elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Only use MQTT IP lookup if the first condition didn't match
# (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
addresses_to_use = mqtt_get_ip(
config, args.username, args.password, args.client_id
)

View File

@@ -304,6 +304,17 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
def _is_framework_url(source: str) -> str:
# platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
import urllib.parse
try:
parsed = urllib.parse.urlparse(source)
except ValueError:
return False
return bool(parsed.scheme)
# NOTE: Keep this in mind when updating the recommended version:
# * New framework historically have had some regressions, especially for WiFi.
# The new version needs to be thoroughly validated before changing the
@@ -387,7 +398,7 @@ def _check_versions(value):
value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version)
)
if value[CONF_SOURCE].startswith("http"):
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = (
f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
)
@@ -400,7 +411,7 @@ def _check_versions(value):
CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
)
if value[CONF_SOURCE].startswith("http"):
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
if CONF_PLATFORM_VERSION not in value:

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from esphome import automation
@@ -52,9 +53,19 @@ class BLEFeatures(StrEnum):
ESP_BT_DEVICE = "ESP_BT_DEVICE"
# Dataclass for registration counts
@dataclass
class RegistrationCounts:
listeners: int = 0
clients: int = 0
# Set to track which features are needed by components
_required_features: set[BLEFeatures] = set()
# Track registration counts for StaticVector sizing
_registration_counts = RegistrationCounts()
def register_ble_features(features: set[BLEFeatures]) -> None:
"""Register BLE features that a component needs.
@@ -257,12 +268,14 @@ async def to_code(config):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
@@ -275,6 +288,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
@@ -287,6 +301,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_SCAN_END, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -320,6 +335,17 @@ async def _add_ble_features():
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
# Add defines for StaticVector sizing based on registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
if _registration_counts.listeners > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
)
if _registration_counts.clients > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
)
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
{
@@ -369,6 +395,7 @@ async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -376,6 +403,7 @@ async def register_ble_device(
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
@@ -389,6 +417,7 @@ async def register_raw_ble_device(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_registration_counts.listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -402,6 +431,7 @@ async def register_raw_client(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_registration_counts.clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var

View File

@@ -74,9 +74,11 @@ void ESP32BLETracker::setup() {
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->disconnect();
}
#endif
}
});
#endif
@@ -206,8 +208,10 @@ void ESP32BLETracker::start_scan_(bool first) {
this->set_scanner_state_(ScannerState::STARTING);
ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
if (!first) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_scan_end();
#endif
}
#ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear();
@@ -236,20 +240,25 @@ void ESP32BLETracker::start_scan_(bool first) {
}
void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_;
this->clients_.push_back(client);
this->recalculate_advertisement_parser_types();
#endif
}
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
listener->set_parent(this);
this->listeners_.push_back(listener);
this->recalculate_advertisement_parser_types();
#endif
}
void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = false;
this->parse_advertisements_ = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) {
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true;
@@ -257,6 +266,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true;
}
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true;
@@ -264,6 +275,7 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true;
}
}
#endif
}
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
@@ -282,10 +294,12 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default:
break;
}
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->gap_event_handler(event, param);
}
#endif
}
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
@@ -348,9 +362,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->gattc_event_handler(event, gattc_if, param);
}
#endif
}
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
@@ -704,12 +720,16 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
// Process raw advertisements
if (this->raw_advertisements_) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1);
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1);
}
#endif
}
// Process parsed advertisements
@@ -719,16 +739,20 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
device.parse_scan_rst(scan_result);
bool found = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
}
}
#endif
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
@@ -745,8 +769,10 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
// Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_scan_end();
#endif
this->set_scanner_state_(ScannerState::IDLE);
}
@@ -770,6 +796,7 @@ void ESP32BLETracker::handle_scanner_failure_() {
void ESP32BLETracker::try_promote_discovered_clients_() {
// Only promote the first discovered client to avoid multiple simultaneous connections
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
if (client->state() != ClientState::DISCOVERED) {
continue;
@@ -791,6 +818,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
client->connect();
break;
}
#endif
}
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {

View File

@@ -302,6 +302,7 @@ class ESP32BLETracker : public Component,
/// Count clients in each state
ClientStateCounts count_client_states_() const {
ClientStateCounts counts;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
switch (client->state()) {
case ClientState::DISCONNECTING:
@@ -317,12 +318,17 @@ class ESP32BLETracker : public Component,
break;
}
}
#endif
return counts;
}
// Group 1: Large objects (12+ bytes) - vectors and callback manager
std::vector<ESPBTDeviceListener *> listeners_;
std::vector<ESPBTClient *> clients_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
StaticVector<ESPBTDeviceListener *, ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT> listeners_;
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
#endif
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
#ifdef USE_ESP32_BLE_DEVICE
/// Vector of addresses that have already been printed in print_bt_device_info

View File

@@ -143,7 +143,6 @@ void ESP32ImprovComponent::loop() {
#else
this->set_state_(improv::STATE_AUTHORIZED);
#endif
this->check_wifi_connection_();
break;
}
case improv::STATE_AUTHORIZED: {
@@ -157,12 +156,31 @@ void ESP32ImprovComponent::loop() {
if (!this->check_identify_()) {
this->set_status_indicator_state_((now % 1000) < 500);
}
this->check_wifi_connection_();
break;
}
case improv::STATE_PROVISIONING: {
this->set_status_indicator_state_((now % 200) < 100);
this->check_wifi_connection_();
if (wifi::global_wifi_component->is_connected()) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
this->set_state_(improv::STATE_PROVISIONED);
std::vector<std::string> urls = {ESPHOME_MY_LINK};
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
this->send_response_(data);
this->stop();
}
break;
}
case improv::STATE_PROVISIONED: {
@@ -374,36 +392,6 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta();
}
void ESP32ImprovComponent::check_wifi_connection_() {
if (!wifi::global_wifi_component->is_connected()) {
return;
}
if (this->state_ == improv::STATE_PROVISIONING) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
std::vector<std::string> urls = {ESPHOME_MY_LINK};
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
this->send_response_(data);
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally");
}
this->set_state_(improv::STATE_PROVISIONED);
this->stop();
}
void ESP32ImprovComponent::advertise_service_data_() {
uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
service_data[0] = IMPROV_PROTOCOL_ID_1; // PR

View File

@@ -111,7 +111,6 @@ class ESP32ImprovComponent : public Component {
void send_response_(std::vector<uint8_t> &response);
void process_incoming_data_();
void on_wifi_connect_timeout_();
void check_wifi_connection_();
bool check_identify_();
void advertise_service_data_();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG

View File

@@ -29,7 +29,7 @@ namespace esphome {
static const char *const TAG = "esphome.ota";
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
#ifdef USE_OTA_PASSWORD

View File

@@ -56,41 +56,50 @@ DriverChip(
"WAVESHARE-P4-86-PANEL",
height=720,
width=720,
hsync_back_porch=50,
hsync_back_porch=80,
hsync_pulse_width=20,
hsync_front_porch=50,
vsync_back_porch=20,
hsync_front_porch=80,
vsync_back_porch=12,
vsync_pulse_width=4,
vsync_front_porch=20,
pclk_frequency="38MHz",
lane_bit_rate="480Mbps",
vsync_front_porch=30,
pclk_frequency="46MHz",
lane_bit_rate="1Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=27,
initsequence=[
(0xB9, 0xF1, 0x12, 0x83),
(0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80),
(0xB2, 0x3C, 0x12, 0x30),
(0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00),
(0xB4, 0x80),
(0xB5, 0x0A, 0x0A),
(0xB6, 0x97, 0x97),
(0xB8, 0x26, 0x22, 0xF0, 0x13),
(0xBA, 0x31, 0x81, 0x0F, 0xF9, 0x0E, 0x06, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37),
(0xBC, 0x47),
(
0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00,
0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37,
),
(0xB8, 0x25, 0x22, 0xF0, 0x63),
(0xBF, 0x02, 0x11, 0x00),
(0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00),
(0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
(0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77),
(0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF),
(0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09),
(0xC8, 0x10, 0x40, 0x1E, 0x02),
(0xCC, 0x0B),
(0xE0, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16),
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x0B, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10),
(0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
(0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
(0xEF, 0xFF, 0xFF, 0x01),
(0x11, 0x00),
(0x29, 0x00),
(0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30),
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,),
(0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33),
(0xB5, 0x0A, 0x0A),
(0xB6, 0xB2, 0xB2),
(
0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80,
0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48,
0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57,
0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
),
(
0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31,
0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64,
0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00,
0x00, 0x00,
),
(
0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14,
0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13,
0x15, 0x14, 0x15, 0x10, 0x17,
),
],
)

View File

@@ -63,8 +63,6 @@ SPIRAM_SPEEDS = {
def supported() -> bool:
if not CORE.is_esp32:
return False
variant = get_esp32_variant()
return variant in SPIRAM_MODES

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from esphome import automation, external_files
import esphome.codegen as cg
from esphome.components import audio, esp32, media_player, psram, speaker
from esphome.components import audio, esp32, media_player, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
@@ -26,21 +26,10 @@ from esphome.const import (
from esphome.core import CORE, HexInt
from esphome.core.entity_helpers import inherit_property_from
from esphome.external_files import download_content
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
def AUTO_LOAD(config: ConfigType) -> list[str]:
load = ["audio"]
if (
not config
or config.get(CONF_TASK_STACK_IN_PSRAM)
or config.get(CONF_CODEC_SUPPORT_ENABLED)
):
return load + ["psram"]
return load
AUTO_LOAD = ["audio", "psram"]
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
DOMAIN = "media_player"
@@ -290,9 +279,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
min=4000, max=4000000
),
cv.Optional(
CONF_CODEC_SUPPORT_ENABLED, default=psram.supported()
): cv.boolean,
cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,

View File

@@ -9,7 +9,6 @@ from esphome.components.esp32 import (
import esphome.config_validation as cv
from esphome.const import CONF_DEVICES, CONF_ID
from esphome.cpp_types import Component
from esphome.types import ConfigType
AUTO_LOAD = ["bytebuffer"]
CODEOWNERS = ["@clydebarrow"]
@@ -21,7 +20,6 @@ USBClient = usb_host_ns.class_("USBClient", Component)
CONF_VID = "vid"
CONF_PID = "pid"
CONF_ENABLE_HUBS = "enable_hubs"
CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests"
def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema:
@@ -46,9 +44,6 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(USBHost),
cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean,
cv.Optional(CONF_MAX_TRANSFER_REQUESTS, default=16): cv.int_range(
min=1, max=32
),
cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
}
),
@@ -63,14 +58,10 @@ async def register_usb_client(config):
return var
async def to_code(config: ConfigType) -> None:
async def to_code(config):
add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
if config.get(CONF_ENABLE_HUBS):
add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True)
max_requests = config[CONF_MAX_TRANSFER_REQUESTS]
cg.add_define("USB_HOST_MAX_REQUESTS", max_requests)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for device in config.get(CONF_DEVICES) or ():

View File

@@ -2,7 +2,6 @@
// Should not be needed, but it's required to pass CI clang-tidy checks
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include <vector>
#include "usb/usb_host.h"
@@ -17,25 +16,23 @@ namespace usb_host {
// THREADING MODEL:
// This component uses a dedicated USB task for event processing to prevent data loss.
// - USB Task (high priority): Handles USB events, executes transfer callbacks, releases transfer slots
// - Main Loop Task: Initiates transfers, processes device connect/disconnect events
// - USB Task (high priority): Handles USB events, executes transfer callbacks
// - Main Loop Task: Initiates transfers, processes completion events
//
// Thread-safe communication:
// - Lock-free queues for USB task -> main loop events (SPSC pattern)
// - Lock-free TransferRequest pool using atomic bitmask (MCMP pattern - multi-consumer, multi-producer)
// - Lock-free TransferRequest pool using atomic bitmask (MCSP pattern)
//
// TransferRequest pool access pattern:
// - get_trq_() [allocate]: Called from BOTH USB task and main loop threads
// * USB task: via USB UART input callbacks that restart transfers immediately
// * Main loop: for output transfers and flow-controlled input restarts
// - release_trq() [deallocate]: Called from BOTH USB task and main loop threads
// * USB task: immediately after transfer callback completes (critical for preventing slot exhaustion)
// * Main loop: when transfer submission fails
// - release_trq() [deallocate]: Called from main loop thread only
//
// The multi-threaded allocation/deallocation is intentional for performance:
// - USB task can immediately restart input transfers and release slots without context switching
// The multi-threaded allocation is intentional for performance:
// - USB task can immediately restart input transfers without context switching
// - Main loop controls backpressure by deciding when to restart after consuming data
// The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking.
// The atomic bitmask ensures thread-safe allocation without mutex blocking.
static const char *const TAG = "usb_host";
@@ -55,17 +52,8 @@ static const uint8_t USB_DIR_IN = 1 << 7;
static const uint8_t USB_DIR_OUT = 0;
static const size_t SETUP_PACKET_SIZE = 8;
static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible.
static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32");
// Select appropriate bitmask type for tracking allocation of TransferRequest slots.
// The bitmask must have at least as many bits as MAX_REQUESTS, so:
// - Use uint16_t for up to 16 requests (MAX_REQUESTS <= 16)
// - Use uint32_t for 17-32 requests (MAX_REQUESTS > 16)
// This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32.
// If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated.
using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type;
static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
static_assert(MAX_REQUESTS <= 16, "MAX_REQUESTS must be <= 16 to fit in uint16_t bitmask");
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5)
@@ -95,6 +83,8 @@ struct TransferRequest {
enum EventType : uint8_t {
EVENT_DEVICE_NEW,
EVENT_DEVICE_GONE,
EVENT_TRANSFER_COMPLETE,
EVENT_CONTROL_COMPLETE,
};
struct UsbEvent {
@@ -106,6 +96,9 @@ struct UsbEvent {
struct {
usb_device_handle_t handle;
} device_gone;
struct {
TransferRequest *trq;
} transfer;
} data;
// Required for EventPool - no cleanup needed for POD types
@@ -170,9 +163,10 @@ class USBClient : public Component {
uint16_t pid_{};
// Lock-free pool management using atomic bitmask (no dynamic allocation)
// Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
// Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
// Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots
std::atomic<trq_bitmask_t> trq_in_use_;
// Supports multiple concurrent consumers (both threads can allocate)
// Single producer for deallocation (main loop only)
// Limited to 16 slots by uint16_t size (enforced by static_assert)
std::atomic<uint16_t> trq_in_use_;
TransferRequest requests_[MAX_REQUESTS]{};
};
class USBHost : public Component {

View File

@@ -228,6 +228,12 @@ void USBClient::loop() {
case EVENT_DEVICE_GONE:
this->on_removed(event->data.device_gone.handle);
break;
case EVENT_TRANSFER_COMPLETE:
case EVENT_CONTROL_COMPLETE: {
auto *trq = event->data.transfer.trq;
this->release_trq(trq);
break;
}
}
// Return event to pool for reuse
this->event_pool.release(event);
@@ -307,6 +313,25 @@ void USBClient::on_removed(usb_device_handle_t handle) {
}
}
// Helper to queue transfer cleanup to main loop
static void queue_transfer_cleanup(TransferRequest *trq, EventType type) {
auto *client = trq->client;
// Allocate event from pool
UsbEvent *event = client->event_pool.allocate();
if (event == nullptr) {
// No events available - increment counter for periodic logging
client->event_queue.increment_dropped_count();
return;
}
event->type = type;
event->data.transfer.trq = trq;
// Push to lock-free queue (always succeeds since pool size == queue size)
client->event_queue.push(event);
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void control_callback(const usb_transfer_t *xfer) {
auto *trq = static_cast<TransferRequest *>(xfer->context);
@@ -321,9 +346,8 @@ static void control_callback(const usb_transfer_t *xfer) {
trq->callback(trq->status);
}
// Release transfer slot immediately in USB task
// The release_trq() uses thread-safe atomic operations
trq->client->release_trq(trq);
// Queue cleanup to main loop
queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE);
}
// THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer)
@@ -334,20 +358,20 @@ static void control_callback(const usb_transfer_t *xfer) {
// This multi-threaded access is intentional for performance - USB task can
// immediately restart transfers without waiting for main loop scheduling.
TransferRequest *USBClient::get_trq_() {
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
uint16_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
// Find first available slot (bit = 0) and try to claim it atomically
// We use a while loop to allow retrying the same slot after CAS failure
size_t i = 0;
while (i != MAX_REQUESTS) {
if (mask & (static_cast<trq_bitmask_t>(1) << i)) {
if (mask & (1U << i)) {
// Slot is in use, move to next slot
i++;
continue;
}
// Slot i appears available, try to claim it atomically
trq_bitmask_t desired = mask | (static_cast<trq_bitmask_t>(1) << i); // Set bit i to mark as in-use
uint16_t desired = mask | (1U << i); // Set bit i to mark as in-use
if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) {
// Successfully claimed slot i - prepare the TransferRequest
@@ -362,7 +386,7 @@ TransferRequest *USBClient::get_trq_() {
i = 0;
}
ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
ESP_LOGE(TAG, "All %d transfer slots in use", MAX_REQUESTS);
return nullptr;
}
void USBClient::disconnect() {
@@ -428,11 +452,8 @@ static void transfer_callback(usb_transfer_t *xfer) {
trq->callback(trq->status);
}
// Release transfer slot AFTER callback completes to prevent slot exhaustion
// This is critical for high-throughput transfers (e.g., USB UART at 115200 baud)
// The callback has finished accessing xfer->data_buffer, so it's safe to release
// The release_trq() uses thread-safe atomic operations
trq->client->release_trq(trq);
// Queue cleanup to main loop
queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE);
}
/**
* Performs a transfer input operation.
@@ -500,12 +521,12 @@ void USBClient::dump_config() {
" Product id %04X",
this->vid_, this->pid_);
}
// THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediately after transfer callback completes
// - Main loop: When transfer submission fails
// THREAD CONTEXT: Only called from main loop thread (single producer for deallocation)
// - Via event processing when handling EVENT_TRANSFER_COMPLETE/EVENT_CONTROL_COMPLETE
// - Directly when transfer submission fails
//
// THREAD SAFETY: Lock-free using atomic AND to clear bit
// Thread-safe atomic operation allows multi-threaded deallocation
// Single-producer pattern makes this simpler than allocation
void USBClient::release_trq(TransferRequest *trq) {
if (trq == nullptr)
return;
@@ -519,8 +540,8 @@ void USBClient::release_trq(TransferRequest *trq) {
// Atomically clear bit i to mark slot as available
// fetch_and with inverted bitmask clears the bit atomically
trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index;
this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release);
uint16_t bit = 1U << index;
this->trq_in_use_.fetch_and(static_cast<uint16_t>(~bit), std::memory_order_release);
}
} // namespace usb_host

View File

@@ -552,7 +552,7 @@ void WiFiComponent::start_scanning() {
// Using insertion sort instead of std::stable_sort saves flash memory
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
static void insertion_sort_scan_results(FixedVector<WiFiScanResult> &results) {
const size_t size = results.size();
for (size_t i = 1; i < size; i++) {
// Make a copy to avoid issues with move semantics during comparison
@@ -576,9 +576,8 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
format_mac_addr_upper(bssid.data(), bssid_s);
if (res.get_matches()) {
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "",
bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi())));
ESP_LOGD(TAG,
" Channel: %u\n"
" RSSI: %d dB",

View File

@@ -278,7 +278,7 @@ class WiFiComponent : public Component {
std::string get_use_address() const;
void set_use_address(const std::string &use_address);
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
const FixedVector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
network::IPAddress wifi_soft_ap_ip();
@@ -385,7 +385,7 @@ class WiFiComponent : public Component {
std::string use_address_;
std::vector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_;
std::vector<WiFiScanResult> scan_result_;
FixedVector<WiFiScanResult> scan_result_;
WiFiAP selected_ap_;
WiFiAP ap_;
optional<float> output_power_;

View File

@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
this->retry_connect();
return;
}
// Count the number of results first
auto *head = reinterpret_cast<bss_info *>(arg);
size_t count = 0;
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
count++;
}
this->scan_result_.init(count);
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,

View File

@@ -763,8 +763,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
const auto &it = data->data.sta_scan_done;
ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
scan_result_.clear();
this->scan_done_ = true;
scan_result_.clear();
if (it.status != 0) {
// scan error
return;
@@ -784,7 +785,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
records.resize(number);
scan_result_.reserve(number);
scan_result_.init(number);
for (int i = 0; i < number; i++) {
auto &record = records[i];
bssid_t bssid;

View File

@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
if (num < 0)
return;
this->scan_result_.reserve(static_cast<unsigned int>(num));
this->scan_result_.init(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.10.0"
__version__ = "2025.11.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -340,8 +340,8 @@ void Application::calculate_looping_components_() {
}
}
// Pre-reserve vector to avoid reallocations
this->looping_components_.reserve(total_looping);
// Initialize FixedVector with exact size - no reallocation possible
this->looping_components_.init(total_looping);
// Add all components with loop override that aren't already LOOP_DONE
// Some components (like logger) may call disable_loop() during initialization

View File

@@ -472,7 +472,7 @@ class Application {
// - When a component is enabled, it's swapped with the first inactive component
// and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop
std::vector<Component *> looping_components_{};
FixedVector<Component *> looping_components_{};
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif

View File

@@ -175,6 +175,8 @@
#define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE
#define USE_ESP32_BLE_SERVER_ON_CONNECT
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
#define USE_ESP32_CAMERA_JPEG_ENCODER
#define USE_I2C
#define USE_IMPROV
@@ -191,7 +193,6 @@
#define USE_WEBSERVER_PORT 80 // NOLINT
#define USE_WEBSERVER_SORTING
#define USE_WIFI_11KV_SUPPORT
#define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)

View File

@@ -159,6 +159,80 @@ template<typename T, size_t N> class StaticVector {
const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
};
/// Fixed-capacity vector - allocates once at runtime, never reallocates
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
/// when size is known at initialization but not at compile time
template<typename T> class FixedVector {
private:
T *data_{nullptr};
size_t size_{0};
size_t capacity_{0};
// Helper to destroy elements and free memory
void cleanup_() {
if (data_ != nullptr) {
// Manually destroy all elements
for (size_t i = 0; i < size_; i++) {
data_[i].~T();
}
// Free raw memory
::operator delete(data_);
}
}
public:
FixedVector() = default;
~FixedVector() { cleanup_(); }
// Disable copy to avoid accidental copies
FixedVector(const FixedVector &) = delete;
FixedVector &operator=(const FixedVector &) = delete;
// Allocate capacity - can be called multiple times to reinit
void init(size_t n) {
cleanup_();
data_ = nullptr;
capacity_ = 0;
size_ = 0;
if (n > 0) {
// Allocate raw memory without calling constructors
data_ = static_cast<T *>(::operator new(n * sizeof(T)));
capacity_ = n;
}
}
// Clear the vector (reset size to 0, keep capacity)
void clear() { size_ = 0; }
// Check if vector is empty
bool empty() const { return size_ == 0; }
/// Add element without bounds checking
/// Caller must ensure sufficient capacity was allocated via init()
/// Silently ignores pushes beyond capacity (no exception or assertion)
void push_back(const T &value) {
if (size_ < capacity_) {
// Use placement new to construct the object in pre-allocated memory
new (&data_[size_]) T(value);
size_++;
}
}
size_t size() const { return size_; }
/// Access element without bounds checking (matches std::vector behavior)
/// Caller must ensure index is valid (i < size())
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
// Iterator support for range-based for loops
T *begin() { return data_; }
T *end() { return data_ + size_; }
const T *begin() const { return data_; }
const T *end() const { return data_ + size_; }
};
///@}
/// @name Mathematics

View File

@@ -410,7 +410,7 @@ def run_ota_impl_(
af, socktype, _, _, sa = r
_LOGGER.info("Connecting to %s port %s...", sa[0], sa[1])
sock = socket.socket(af, socktype)
sock.settimeout(20.0)
sock.settimeout(10.0)
try:
sock.connect(sa)
except OSError as err:

View File

@@ -15,8 +15,6 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError
from esphome.helpers import (
copy_file_if_changed,
get_str_env,
is_ha_addon,
read_file,
walk_files,
write_file_if_changed,
@@ -340,21 +338,16 @@ def clean_build():
def clean_all(configuration: list[str]):
import shutil
data_dirs = [Path(dir) / ".esphome" for dir in configuration]
if is_ha_addon():
data_dirs.append(Path("/data"))
if "ESPHOME_DATA_DIR" in os.environ:
data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None)))
# Clean build dir
for dir in data_dirs:
if dir.is_dir():
_LOGGER.info("Cleaning %s", dir)
# Don't remove storage or .json files which are needed by the dashboard
for item in dir.iterdir():
if item.is_file() and not item.name.endswith(".json"):
# Clean entire build dir
for dir in configuration:
build_dir = Path(dir) / ".esphome"
if build_dir.is_dir():
_LOGGER.info("Cleaning %s", build_dir)
# Don't remove storage as it will cause the dashboard to regenerate all configs
for item in build_dir.iterdir():
if item.is_file():
item.unlink()
elif item.is_dir() and item.name != "storage":
elif item.name != "storage" and item.is_dir():
shutil.rmtree(item)
# Clean PlatformIO project files

View File

@@ -1,4 +1,3 @@
[build]
command = "script/build-api-docs"
publish = "api-docs"
environment = { PYTHON_VERSION = "3.13" }

View File

@@ -11,8 +11,8 @@ pyserial==3.5
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
aioesphomeapi==41.16.1
esphome-dashboard==20251009.0
aioesphomeapi==41.13.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -1,7 +1,7 @@
pylint==3.3.9
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests

View File

@@ -40,7 +40,9 @@ display:
- number: 17
blue:
- number: 47
- number: 1
allow_other_uses: true
- number: 41
allow_other_uses: true
- number: 0
ignore_strapping_warning: true
- number: 42
@@ -51,7 +53,7 @@ display:
number: 45
ignore_strapping_warning: true
hsync_pin:
number: 38
number: 40
vsync_pin:
number: 48
data_rate: 1000000.0

View File

@@ -1,7 +1,6 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
flow_control_pin: GPIO13
packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml

View File

@@ -1,7 +1,6 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
flow_control_pin: GPIO13
packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml

View File

@@ -1,7 +1,6 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
flow_control_pin: GPIO13
packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml

View File

@@ -1,5 +1,4 @@
usb_host:
max_transfer_requests: 32 # Test uint32_t bitmask path (17-32 requests)
devices:
- id: device_1
vid: 0x1234

View File

@@ -493,7 +493,7 @@ def test_run_ota_impl_successful(
assert result_host == "192.168.1.100"
# Verify socket was configured correctly
mock_socket.settimeout.assert_called_with(20.0)
mock_socket.settimeout.assert_called_with(10.0)
mock_socket.connect.assert_called_once_with(("192.168.1.100", 3232))
mock_socket.close.assert_called_once()

View File

@@ -1203,31 +1203,6 @@ def test_show_logs_api(
)
@patch("esphome.components.api.client.run_logs")
def test_show_logs_api_with_fqdn_mdns_disabled(
mock_run_logs: Mock,
) -> None:
"""Test show_logs with API using FQDN when mDNS is disabled."""
setup_core(
config={
"logger": {},
CONF_API: {},
CONF_MDNS: {CONF_DISABLED: True},
},
platform=PLATFORM_ESP32,
)
mock_run_logs.return_value = 0
args = MockArgs()
devices = ["device.example.com"]
result = show_logs(CORE.config, args, devices)
assert result == 0
# Should use the FQDN directly, not try MQTT lookup
mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"])
@patch("esphome.components.api.client.run_logs")
def test_show_logs_api_with_mqtt_fallback(
mock_run_logs: Mock,
@@ -1247,7 +1222,7 @@ def test_show_logs_api_with_mqtt_fallback(
mock_mqtt_get_ip.return_value = ["192.168.1.200"]
args = MockArgs(username="user", password="pass", client_id="client")
devices = ["MQTTIP"]
devices = ["device.local"]
result = show_logs(CORE.config, args, devices)
@@ -1512,31 +1487,27 @@ def test_mqtt_get_ip() -> None:
def test_has_resolvable_address() -> None:
"""Test has_resolvable_address function."""
# Test with mDNS enabled and .local hostname address
# Test with mDNS enabled and hostname address
setup_core(config={}, address="esphome-device.local")
assert has_resolvable_address() is True
# Test with mDNS disabled and .local hostname address (still resolvable via DNS)
# Test with mDNS disabled and hostname address
setup_core(
config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local"
)
assert has_resolvable_address() is True
assert has_resolvable_address() is False
# Test with mDNS disabled and regular DNS hostname (resolvable)
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com")
assert has_resolvable_address() is True
# Test with IP address (always resolvable, mDNS doesn't matter)
# Test with IP address (mDNS doesn't matter)
setup_core(config={}, address="192.168.1.100")
assert has_resolvable_address() is True
# Test with IP address and mDNS disabled (still resolvable)
# Test with IP address and mDNS disabled
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100")
assert has_resolvable_address() is True
# Test with no address
# Test with no address but mDNS enabled (can still resolve mDNS names)
setup_core(config={}, address=None)
assert has_resolvable_address() is False
assert has_resolvable_address() is True
# Test with no address and mDNS disabled
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None)

View File

@@ -985,49 +985,3 @@ def test_clean_all_removes_non_storage_directories(
# Verify logging mentions cleaning
assert "Cleaning" in caplog.text
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_preserves_json_files(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all preserves .json files."""
# Create build directory with various files
config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir()
# Create .json files (should be preserved)
(build_dir / "config.json").write_text('{"config": "data"}')
(build_dir / "metadata.json").write_text('{"metadata": "info"}')
# Create non-.json files (should be removed)
(build_dir / "dummy.txt").write_text("x")
(build_dir / "other.log").write_text("log content")
# Call clean_all
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(config_dir)])
# Verify .esphome directory still exists
assert build_dir.exists()
# Verify .json files are preserved
assert (build_dir / "config.json").exists()
assert (build_dir / "config.json").read_text() == '{"config": "data"}'
assert (build_dir / "metadata.json").exists()
assert (build_dir / "metadata.json").read_text() == '{"metadata": "info"}'
# Verify non-.json files were removed
assert not (build_dir / "dummy.txt").exists()
assert not (build_dir / "other.log").exists()
# Verify logging mentions cleaning
assert "Cleaning" in caplog.text
assert str(build_dir) in caplog.text