mirror of
https://github.com/esphome/esphome.git
synced 2025-11-10 11:55:52 +00:00
Compare commits
37 Commits
2025.10.0
...
wifi_fixed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
347501d895 | ||
|
|
4c00861760 | ||
|
|
2ff3e7fb2b | ||
|
|
b0c20d7adb | ||
|
|
2cc5e24b38 | ||
|
|
3afa73b449 | ||
|
|
dcf2697a2a | ||
|
|
6a11700a6b | ||
|
|
9bd9b043c8 | ||
|
|
cb602c9b1a | ||
|
|
b54beb357a | ||
|
|
6abc2efd96 | ||
|
|
be51093a7e | ||
|
|
52219c4dcc | ||
|
|
590cae13c0 | ||
|
|
e15429b0f5 | ||
|
|
b5cc668a45 | ||
|
|
a1b0ae78e0 | ||
|
|
fcc8a809e6 | ||
|
|
48474c0f8c | ||
|
|
9f9c95dd09 | ||
|
|
a74fcbc8b6 | ||
|
|
c8b898f9c5 | ||
|
|
81bf2688b4 | ||
|
|
87d2c9868f | ||
|
|
5ca407e27c | ||
|
|
5bbc2ab482 | ||
|
|
309e8b4c92 | ||
|
|
eee2987c99 | ||
|
|
061e55f8c5 | ||
|
|
56334b7832 | ||
|
|
a4b7e0c700 | ||
|
|
84ad7ee0e4 | ||
|
|
d006008539 | ||
|
|
6bb1e4c9c0 | ||
|
|
82bdb08884 | ||
|
|
b709ff84c3 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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:
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -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
|
||||||
|
|||||||
2
Doxyfile
2
Doxyfile
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 ():
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user