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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # 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 # 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 # 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: def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" """Check if CORE.address is resolvable (via mDNS or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable return has_mdns() or has_ip_address()
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
return CORE.address is not None
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): 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(): if has_api():
addresses_to_use: list[str] | None = None addresses_to_use: list[str] | None = None
if port_type == "NETWORK": if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
# Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used
# The resolve_ip_address() function in helpers.py handles all types
addresses_to_use = devices addresses_to_use = devices
elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup(): elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Use MQTT IP lookup for MQTT/MQTTIP types # 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( addresses_to_use = mqtt_get_ip(
config, args.username, args.password, args.client_id 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" 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: # NOTE: Keep this in mind when updating the recommended version:
# * New framework historically have had some regressions, especially for WiFi. # * New framework historically have had some regressions, especially for WiFi.
# The new version needs to be thoroughly validated before changing the # The new version needs to be thoroughly validated before changing the
@@ -387,7 +398,7 @@ def _check_versions(value):
value[CONF_SOURCE] = value.get( value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version) CONF_SOURCE, _format_framework_arduino_version(version)
) )
if value[CONF_SOURCE].startswith("http"): if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = ( value[CONF_SOURCE] = (
f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
) )
@@ -400,7 +411,7 @@ def _check_versions(value):
CONF_SOURCE, CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), _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]}" value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
if CONF_PLATFORM_VERSION not in value: if CONF_PLATFORM_VERSION not in value:

View File

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

View File

@@ -74,9 +74,11 @@ void ESP32BLETracker::setup() {
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) { if (state == ota::OTA_STARTED) {
this->stop_scan(); this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->disconnect(); client->disconnect();
} }
#endif
} }
}); });
#endif #endif
@@ -206,8 +208,10 @@ void ESP32BLETracker::start_scan_(bool first) {
this->set_scanner_state_(ScannerState::STARTING); this->set_scanner_state_(ScannerState::STARTING);
ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING."); ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
if (!first) { if (!first) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
#endif
} }
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear(); this->already_discovered_.clear();
@@ -236,20 +240,25 @@ void ESP32BLETracker::start_scan_(bool first) {
} }
void ESP32BLETracker::register_client(ESPBTClient *client) { void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_; client->app_id = ++this->app_id_;
this->clients_.push_back(client); this->clients_.push_back(client);
this->recalculate_advertisement_parser_types(); this->recalculate_advertisement_parser_types();
#endif
} }
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
listener->set_parent(this); listener->set_parent(this);
this->listeners_.push_back(listener); this->listeners_.push_back(listener);
this->recalculate_advertisement_parser_types(); this->recalculate_advertisement_parser_types();
#endif
} }
void ESP32BLETracker::recalculate_advertisement_parser_types() { void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = false; this->raw_advertisements_ = false;
this->parse_advertisements_ = false; this->parse_advertisements_ = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true; this->parse_advertisements_ = true;
@@ -257,6 +266,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true; this->raw_advertisements_ = true;
} }
} }
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true; this->parse_advertisements_ = true;
@@ -264,6 +275,7 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true; this->raw_advertisements_ = true;
} }
} }
#endif
} }
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { 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: default:
break; 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_) { for (auto *client : this->clients_) {
client->gap_event_handler(event, param); client->gap_event_handler(event, param);
} }
#endif
} }
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { 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, void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->gattc_event_handler(event, gattc_if, param); client->gattc_event_handler(event, gattc_if, param);
} }
#endif
} }
void ESP32BLETracker::set_scanner_state_(ScannerState state) { 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) { void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
// Process raw advertisements // Process raw advertisements
if (this->raw_advertisements_) { if (this->raw_advertisements_) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1); listener->parse_devices(&scan_result, 1);
} }
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1); client->parse_devices(&scan_result, 1);
} }
#endif
} }
// Process parsed advertisements // Process parsed advertisements
@@ -719,16 +739,20 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
device.parse_scan_rst(scan_result); device.parse_scan_rst(scan_result);
bool found = false; bool found = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
if (listener->parse_device(device)) if (listener->parse_device(device))
found = true; found = true;
} }
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->parse_device(device)) { if (client->parse_device(device)) {
found = true; found = true;
} }
} }
#endif
if (!found && !this->scan_continuous_) { if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device); 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 // Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
#endif
this->set_scanner_state_(ScannerState::IDLE); this->set_scanner_state_(ScannerState::IDLE);
} }
@@ -770,6 +796,7 @@ void ESP32BLETracker::handle_scanner_failure_() {
void ESP32BLETracker::try_promote_discovered_clients_() { void ESP32BLETracker::try_promote_discovered_clients_() {
// Only promote the first discovered client to avoid multiple simultaneous connections // Only promote the first discovered client to avoid multiple simultaneous connections
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->state() != ClientState::DISCOVERED) { if (client->state() != ClientState::DISCOVERED) {
continue; continue;
@@ -791,6 +818,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
client->connect(); client->connect();
break; break;
} }
#endif
} }
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const { 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 /// Count clients in each state
ClientStateCounts count_client_states_() const { ClientStateCounts count_client_states_() const {
ClientStateCounts counts; ClientStateCounts counts;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
switch (client->state()) { switch (client->state()) {
case ClientState::DISCONNECTING: case ClientState::DISCONNECTING:
@@ -317,12 +318,17 @@ class ESP32BLETracker : public Component,
break; break;
} }
} }
#endif
return counts; return counts;
} }
// Group 1: Large objects (12+ bytes) - vectors and callback manager // Group 1: Large objects (12+ bytes) - vectors and callback manager
std::vector<ESPBTDeviceListener *> listeners_; #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
std::vector<ESPBTClient *> clients_; 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_; CallbackManager<void(ScannerState)> scanner_state_callbacks_;
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
/// Vector of addresses that have already been printed in print_bt_device_info /// Vector of addresses that have already been printed in print_bt_device_info

View File

@@ -143,7 +143,6 @@ void ESP32ImprovComponent::loop() {
#else #else
this->set_state_(improv::STATE_AUTHORIZED); this->set_state_(improv::STATE_AUTHORIZED);
#endif #endif
this->check_wifi_connection_();
break; break;
} }
case improv::STATE_AUTHORIZED: { case improv::STATE_AUTHORIZED: {
@@ -157,12 +156,31 @@ void ESP32ImprovComponent::loop() {
if (!this->check_identify_()) { if (!this->check_identify_()) {
this->set_status_indicator_state_((now % 1000) < 500); this->set_status_indicator_state_((now % 1000) < 500);
} }
this->check_wifi_connection_();
break; break;
} }
case improv::STATE_PROVISIONING: { case improv::STATE_PROVISIONING: {
this->set_status_indicator_state_((now % 200) < 100); 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; break;
} }
case improv::STATE_PROVISIONED: { case improv::STATE_PROVISIONED: {
@@ -374,36 +392,6 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta(); 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_() { void ESP32ImprovComponent::advertise_service_data_() {
uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {}; uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
service_data[0] = IMPROV_PROTOCOL_ID_1; // PR 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 send_response_(std::vector<uint8_t> &response);
void process_incoming_data_(); void process_incoming_data_();
void on_wifi_connect_timeout_(); void on_wifi_connect_timeout_();
void check_wifi_connection_();
bool check_identify_(); bool check_identify_();
void advertise_service_data_(); void advertise_service_data_();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG

View File

@@ -29,7 +29,7 @@ namespace esphome {
static const char *const TAG = "esphome.ota"; static const char *const TAG = "esphome.ota";
static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer 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 static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD

View File

@@ -56,41 +56,50 @@ DriverChip(
"WAVESHARE-P4-86-PANEL", "WAVESHARE-P4-86-PANEL",
height=720, height=720,
width=720, width=720,
hsync_back_porch=50, hsync_back_porch=80,
hsync_pulse_width=20, hsync_pulse_width=20,
hsync_front_porch=50, hsync_front_porch=80,
vsync_back_porch=20, vsync_back_porch=12,
vsync_pulse_width=4, vsync_pulse_width=4,
vsync_front_porch=20, vsync_front_porch=30,
pclk_frequency="38MHz", pclk_frequency="46MHz",
lane_bit_rate="480Mbps", lane_bit_rate="1Gbps",
swap_xy=cv.UNDEFINED, swap_xy=cv.UNDEFINED,
color_order="RGB", color_order="RGB",
reset_pin=27, reset_pin=27,
initsequence=[ initsequence=[
(0xB9, 0xF1, 0x12, 0x83), (0xB9, 0xF1, 0x12, 0x83),
(0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80), (
(0xB2, 0x3C, 0x12, 0x30), 0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00,
(0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00), 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37,
(0xB4, 0x80), ),
(0xB5, 0x0A, 0x0A), (0xB8, 0x25, 0x22, 0xF0, 0x63),
(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),
(0xBF, 0x02, 0x11, 0x00), (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), (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
(0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77), (0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30),
(0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF), (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,),
(0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09), (0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33),
(0xC8, 0x10, 0x40, 0x1E, 0x02), (0xB5, 0x0A, 0x0A),
(0xCC, 0x0B), (0xB6, 0xB2, 0xB2),
(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, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80,
(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), 0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48,
(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), 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57,
(0xEF, 0xFF, 0xFF, 0x01), 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
(0x11, 0x00), 0x00, 0x00, 0x00, 0x00,
(0x29, 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: def supported() -> bool:
if not CORE.is_esp32:
return False
variant = get_esp32_variant() variant = get_esp32_variant()
return variant in SPIRAM_MODES return variant in SPIRAM_MODES

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from esphome import automation, external_files from esphome import automation, external_files
import esphome.codegen as cg 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 import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
@@ -26,21 +26,10 @@ from esphome.const import (
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
from esphome.external_files import download_content from esphome.external_files import download_content
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["audio", "psram"]
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
CODEOWNERS = ["@kahrendt", "@synesthesiam"] CODEOWNERS = ["@kahrendt", "@synesthesiam"]
DOMAIN = "media_player" DOMAIN = "media_player"
@@ -290,9 +279,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
min=4000, max=4000000 min=4000, max=4000000
), ),
cv.Optional( cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean,
CONF_CODEC_SUPPORT_ENABLED, default=psram.supported()
): cv.boolean,
cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), 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_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, 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 import esphome.config_validation as cv
from esphome.const import CONF_DEVICES, CONF_ID from esphome.const import CONF_DEVICES, CONF_ID
from esphome.cpp_types import Component from esphome.cpp_types import Component
from esphome.types import ConfigType
AUTO_LOAD = ["bytebuffer"] AUTO_LOAD = ["bytebuffer"]
CODEOWNERS = ["@clydebarrow"] CODEOWNERS = ["@clydebarrow"]
@@ -21,7 +20,6 @@ USBClient = usb_host_ns.class_("USBClient", Component)
CONF_VID = "vid" CONF_VID = "vid"
CONF_PID = "pid" CONF_PID = "pid"
CONF_ENABLE_HUBS = "enable_hubs" 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: 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.GenerateID(): cv.declare_id(USBHost),
cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean, 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()), cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
} }
), ),
@@ -63,14 +58,10 @@ async def register_usb_client(config):
return var 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) add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
if config.get(CONF_ENABLE_HUBS): if config.get(CONF_ENABLE_HUBS):
add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) 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]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
for device in config.get(CONF_DEVICES) or (): 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 // 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) #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 "esphome/core/component.h"
#include <vector> #include <vector>
#include "usb/usb_host.h" #include "usb/usb_host.h"
@@ -17,25 +16,23 @@ namespace usb_host {
// THREADING MODEL: // THREADING MODEL:
// This component uses a dedicated USB task for event processing to prevent data loss. // 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 // - USB Task (high priority): Handles USB events, executes transfer callbacks
// - Main Loop Task: Initiates transfers, processes device connect/disconnect events // - Main Loop Task: Initiates transfers, processes completion events
// //
// Thread-safe communication: // Thread-safe communication:
// - Lock-free queues for USB task -> main loop events (SPSC pattern) // - 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: // TransferRequest pool access pattern:
// - get_trq_() [allocate]: Called from BOTH USB task and main loop threads // - get_trq_() [allocate]: Called from BOTH USB task and main loop threads
// * USB task: via USB UART input callbacks that restart transfers immediately // * USB task: via USB UART input callbacks that restart transfers immediately
// * Main loop: for output transfers and flow-controlled input restarts // * Main loop: for output transfers and flow-controlled input restarts
// - release_trq() [deallocate]: Called from BOTH USB task and main loop threads // - release_trq() [deallocate]: Called from main loop thread only
// * USB task: immediately after transfer callback completes (critical for preventing slot exhaustion)
// * Main loop: when transfer submission fails
// //
// The multi-threaded allocation/deallocation is intentional for performance: // The multi-threaded allocation is intentional for performance:
// - USB task can immediately restart input transfers and release slots without context switching // - USB task can immediately restart input transfers without context switching
// - Main loop controls backpressure by deciding when to restart after consuming data // - 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"; 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 uint8_t USB_DIR_OUT = 0;
static const size_t SETUP_PACKET_SIZE = 8; 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 const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); static_assert(MAX_REQUESTS <= 16, "MAX_REQUESTS must be <= 16 to fit in uint16_t bitmask");
// 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 constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop 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 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) 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 { enum EventType : uint8_t {
EVENT_DEVICE_NEW, EVENT_DEVICE_NEW,
EVENT_DEVICE_GONE, EVENT_DEVICE_GONE,
EVENT_TRANSFER_COMPLETE,
EVENT_CONTROL_COMPLETE,
}; };
struct UsbEvent { struct UsbEvent {
@@ -106,6 +96,9 @@ struct UsbEvent {
struct { struct {
usb_device_handle_t handle; usb_device_handle_t handle;
} device_gone; } device_gone;
struct {
TransferRequest *trq;
} transfer;
} data; } data;
// Required for EventPool - no cleanup needed for POD types // Required for EventPool - no cleanup needed for POD types
@@ -170,9 +163,10 @@ class USBClient : public Component {
uint16_t pid_{}; uint16_t pid_{};
// Lock-free pool management using atomic bitmask (no dynamic allocation) // 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 // 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) // Supports multiple concurrent consumers (both threads can allocate)
// Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots // Single producer for deallocation (main loop only)
std::atomic<trq_bitmask_t> trq_in_use_; // Limited to 16 slots by uint16_t size (enforced by static_assert)
std::atomic<uint16_t> trq_in_use_;
TransferRequest requests_[MAX_REQUESTS]{}; TransferRequest requests_[MAX_REQUESTS]{};
}; };
class USBHost : public Component { class USBHost : public Component {

View File

@@ -228,6 +228,12 @@ void USBClient::loop() {
case EVENT_DEVICE_GONE: case EVENT_DEVICE_GONE:
this->on_removed(event->data.device_gone.handle); this->on_removed(event->data.device_gone.handle);
break; 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 // Return event to pool for reuse
this->event_pool.release(event); 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) // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void control_callback(const usb_transfer_t *xfer) { static void control_callback(const usb_transfer_t *xfer) {
auto *trq = static_cast<TransferRequest *>(xfer->context); auto *trq = static_cast<TransferRequest *>(xfer->context);
@@ -321,9 +346,8 @@ static void control_callback(const usb_transfer_t *xfer) {
trq->callback(trq->status); trq->callback(trq->status);
} }
// Release transfer slot immediately in USB task // Queue cleanup to main loop
// The release_trq() uses thread-safe atomic operations queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE);
trq->client->release_trq(trq);
} }
// THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer) // 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 // This multi-threaded access is intentional for performance - USB task can
// immediately restart transfers without waiting for main loop scheduling. // immediately restart transfers without waiting for main loop scheduling.
TransferRequest *USBClient::get_trq_() { 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 // 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 // We use a while loop to allow retrying the same slot after CAS failure
size_t i = 0; size_t i = 0;
while (i != MAX_REQUESTS) { 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 // Slot is in use, move to next slot
i++; i++;
continue; continue;
} }
// Slot i appears available, try to claim it atomically // 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)) { 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 // Successfully claimed slot i - prepare the TransferRequest
@@ -362,7 +386,7 @@ TransferRequest *USBClient::get_trq_() {
i = 0; 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; return nullptr;
} }
void USBClient::disconnect() { void USBClient::disconnect() {
@@ -428,11 +452,8 @@ static void transfer_callback(usb_transfer_t *xfer) {
trq->callback(trq->status); trq->callback(trq->status);
} }
// Release transfer slot AFTER callback completes to prevent slot exhaustion // Queue cleanup to main loop
// This is critical for high-throughput transfers (e.g., USB UART at 115200 baud) queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE);
// 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);
} }
/** /**
* Performs a transfer input operation. * Performs a transfer input operation.
@@ -500,12 +521,12 @@ void USBClient::dump_config() {
" Product id %04X", " Product id %04X",
this->vid_, this->pid_); this->vid_, this->pid_);
} }
// THREAD CONTEXT: Called from both USB task and main loop threads // THREAD CONTEXT: Only called from main loop thread (single producer for deallocation)
// - USB task: Immediately after transfer callback completes // - Via event processing when handling EVENT_TRANSFER_COMPLETE/EVENT_CONTROL_COMPLETE
// - Main loop: When transfer submission fails // - Directly when transfer submission fails
// //
// THREAD SAFETY: Lock-free using atomic AND to clear bit // 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) { void USBClient::release_trq(TransferRequest *trq) {
if (trq == nullptr) if (trq == nullptr)
return; return;
@@ -519,8 +540,8 @@ void USBClient::release_trq(TransferRequest *trq) {
// Atomically clear bit i to mark slot as available // Atomically clear bit i to mark slot as available
// fetch_and with inverted bitmask clears the bit atomically // fetch_and with inverted bitmask clears the bit atomically
trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index; uint16_t bit = 1U << index;
this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release); this->trq_in_use_.fetch_and(static_cast<uint16_t>(~bit), std::memory_order_release);
} }
} // namespace usb_host } // namespace usb_host

View File

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

View File

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

View File

@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
this->retry_connect(); this->retry_connect();
return; return;
} }
// Count the number of results first
auto *head = reinterpret_cast<bss_info *>(arg); 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)) { 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]}, 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, 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; 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); 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; this->scan_done_ = true;
scan_result_.clear();
if (it.status != 0) { if (it.status != 0) {
// scan error // scan error
return; return;
@@ -784,7 +785,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} }
records.resize(number); records.resize(number);
scan_result_.reserve(number); scan_result_.init(number);
for (int i = 0; i < number; i++) { for (int i = 0; i < number; i++) {
auto &record = records[i]; auto &record = records[i];
bssid_t bssid; bssid_t bssid;

View File

@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
if (num < 0) if (num < 0)
return; 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++) { for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i); String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i); wifi_auth_mode_t authmode = WiFi.encryptionType(i);

View File

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

View File

@@ -340,8 +340,8 @@ void Application::calculate_looping_components_() {
} }
} }
// Pre-reserve vector to avoid reallocations // Initialize FixedVector with exact size - no reallocation possible
this->looping_components_.reserve(total_looping); this->looping_components_.init(total_looping);
// Add all components with loop override that aren't already LOOP_DONE // Add all components with loop override that aren't already LOOP_DONE
// Some components (like logger) may call disable_loop() during initialization // 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 // - When a component is enabled, it's swapped with the first inactive component
// and active_end_ is incremented // and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop // - 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 #ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif #endif

View File

@@ -175,6 +175,8 @@
#define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE #define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE
#define USE_ESP32_BLE_SERVER_ON_CONNECT #define USE_ESP32_BLE_SERVER_ON_CONNECT
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT #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_ESP32_CAMERA_JPEG_ENCODER
#define USE_I2C #define USE_I2C
#define USE_IMPROV #define USE_IMPROV
@@ -191,7 +193,6 @@
#define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_PORT 80 // NOLINT
#define USE_WEBSERVER_SORTING #define USE_WEBSERVER_SORTING
#define USE_WIFI_11KV_SUPPORT #define USE_WIFI_11KV_SUPPORT
#define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) #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()); } 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 /// @name Mathematics

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
[build] [build]
command = "script/build-api-docs" command = "script/build-api-docs"
publish = "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 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0 esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20251013.0 esphome-dashboard==20251009.0
aioesphomeapi==41.16.1 aioesphomeapi==41.13.0
zeroconf==0.148.0 zeroconf==0.148.0
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -1,7 +1,7 @@
pylint==3.3.9 pylint==3.3.9
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating 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 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 pre-commit
# Unit tests # Unit tests

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -493,7 +493,7 @@ def test_run_ota_impl_successful(
assert result_host == "192.168.1.100" assert result_host == "192.168.1.100"
# Verify socket was configured correctly # 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.connect.assert_called_once_with(("192.168.1.100", 3232))
mock_socket.close.assert_called_once() 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") @patch("esphome.components.api.client.run_logs")
def test_show_logs_api_with_mqtt_fallback( def test_show_logs_api_with_mqtt_fallback(
mock_run_logs: Mock, 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"] mock_mqtt_get_ip.return_value = ["192.168.1.200"]
args = MockArgs(username="user", password="pass", client_id="client") args = MockArgs(username="user", password="pass", client_id="client")
devices = ["MQTTIP"] devices = ["device.local"]
result = show_logs(CORE.config, args, devices) result = show_logs(CORE.config, args, devices)
@@ -1512,31 +1487,27 @@ def test_mqtt_get_ip() -> None:
def test_has_resolvable_address() -> None: def test_has_resolvable_address() -> None:
"""Test has_resolvable_address function.""" """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") setup_core(config={}, address="esphome-device.local")
assert has_resolvable_address() is True 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( setup_core(
config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local" 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) # Test with IP address (mDNS doesn't matter)
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)
setup_core(config={}, address="192.168.1.100") setup_core(config={}, address="192.168.1.100")
assert has_resolvable_address() is True 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") setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100")
assert has_resolvable_address() is True 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) setup_core(config={}, address=None)
assert has_resolvable_address() is False assert has_resolvable_address() is True
# Test with no address and mDNS disabled # Test with no address and mDNS disabled
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None) 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 # Verify logging mentions cleaning
assert "Cleaning" in caplog.text assert "Cleaning" in caplog.text
assert str(build_dir) 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