mirror of
https://github.com/esphome/esphome.git
synced 2025-11-18 15:55:46 +00:00
Compare commits
21 Commits
memory_api
...
2025.11.0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d71b6b93e | ||
|
|
298813d4fa | ||
|
|
56d141c741 | ||
|
|
47a7f729dd | ||
|
|
7806eb980f | ||
|
|
a59888224c | ||
|
|
58ad4759f0 | ||
|
|
87f79290ba | ||
|
|
9326d78439 | ||
|
|
a93887a790 | ||
|
|
d7fa131a8a | ||
|
|
79a4444928 | ||
|
|
572fae5c7d | ||
|
|
5dafaaced4 | ||
|
|
65a303d48f | ||
|
|
00c71b7236 | ||
|
|
ef04903a7a | ||
|
|
a2ec7f622c | ||
|
|
2f91e7bd47 | ||
|
|
80a7c6d3c3 | ||
|
|
7a92565a0c |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.11.0-dev
|
||||
PROJECT_NUMBER = 2025.11.0b1
|
||||
|
||||
# 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
|
||||
|
||||
@@ -741,13 +741,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Set memory analysis options in config
|
||||
if args.analyze_memory:
|
||||
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
|
||||
|
||||
if args.memory_report:
|
||||
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
|
||||
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -1209,17 +1202,6 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--analyze-memory",
|
||||
help="Analyze and display memory usage by component after compilation.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--memory-report",
|
||||
help="Save memory analysis report to a file (supports .json or .txt).",
|
||||
type=str,
|
||||
metavar="FILE",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""CLI interface for memory analysis with report generation."""
|
||||
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import sys
|
||||
|
||||
from . import (
|
||||
@@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Export analysis results as JSON."""
|
||||
data = {
|
||||
"components": {
|
||||
name: {
|
||||
"text": mem.text_size,
|
||||
"rodata": mem.rodata_size,
|
||||
"data": mem.data_size,
|
||||
"bss": mem.bss_size,
|
||||
"flash_total": mem.flash_total,
|
||||
"ram_total": mem.ram_total,
|
||||
"symbol_count": mem.symbol_count,
|
||||
}
|
||||
for name, mem in self.components.items()
|
||||
},
|
||||
"totals": {
|
||||
"flash": sum(c.flash_total for c in self.components.values()),
|
||||
"ram": sum(c.ram_total for c in self.components.values()),
|
||||
},
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||
"""Dump uncategorized symbols for analysis."""
|
||||
# Sort by size descending
|
||||
|
||||
@@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
||||
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
||||
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
}
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single) {
|
||||
EventResponse resp;
|
||||
resp.set_event_type(StringRef(event_type));
|
||||
@@ -1650,7 +1650,9 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
||||
// O(n) but optimized for RAM and not performance.
|
||||
for (auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type) {
|
||||
// Replace with new creator
|
||||
// Clean up old creator before replacing
|
||||
item.creator.cleanup(message_type);
|
||||
// Move assign the new creator
|
||||
item.creator = std::move(creator);
|
||||
return;
|
||||
}
|
||||
@@ -1820,7 +1822,7 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning
|
||||
// Remove processed items from the beginning with proper cleanup
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
@@ -1833,10 +1835,10 @@ void APIConnection::process_batch_() {
|
||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single, uint8_t message_type) const {
|
||||
#ifdef USE_EVENT
|
||||
// Special case: EventResponse uses const char * pointer
|
||||
// Special case: EventResponse uses string pointer
|
||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
||||
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event, const char *event_type);
|
||||
void send_event(event::Event *event, const std::string &event_type);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
@@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection {
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
#endif
|
||||
@@ -508,8 +508,10 @@ class APIConnection final : public APIServerConnection {
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
|
||||
// Constructor for const char * (Event types - no allocation needed)
|
||||
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
||||
// Constructor for string state capture
|
||||
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
|
||||
|
||||
// No destructor - cleanup must be called explicitly with message_type
|
||||
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
@@ -521,6 +523,8 @@ class APIConnection final : public APIServerConnection {
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
||||
// In our usage, this happens in add_item() deduplication and vector::erase()
|
||||
data_ = other.data_;
|
||||
other.data_.function_ptr = nullptr;
|
||||
}
|
||||
@@ -531,10 +535,20 @@ class APIConnection final : public APIServerConnection {
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint8_t message_type) const;
|
||||
|
||||
// Manual cleanup method - must be called before destruction for string types
|
||||
void cleanup(uint8_t message_type) {
|
||||
#ifdef USE_EVENT
|
||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
||||
delete data_.string_ptr;
|
||||
data_.string_ptr = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
const char *const_char_ptr;
|
||||
std::string *string_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
};
|
||||
|
||||
@@ -554,24 +568,42 @@ class APIConnection final : public APIServerConnection {
|
||||
std::vector<BatchItem> items;
|
||||
uint32_t batch_start_time{0};
|
||||
|
||||
private:
|
||||
// Helper to cleanup items from the beginning
|
||||
void cleanup_items_(size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
items[i].creator.cleanup(items[i].message_type);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DeferredBatch() {
|
||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||
items.reserve(8);
|
||||
}
|
||||
|
||||
~DeferredBatch() {
|
||||
// Ensure cleanup of any remaining items
|
||||
clear();
|
||||
}
|
||||
|
||||
// Add item to the batch
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
|
||||
// Clear all items
|
||||
// Clear all items with proper cleanup
|
||||
void clear() {
|
||||
cleanup_items_(items.size());
|
||||
items.clear();
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front
|
||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||
// Remove processed items from the front with proper cleanup
|
||||
void remove_front(size_t count) {
|
||||
cleanup_items_(count);
|
||||
items.erase(items.begin(), items.begin() + count);
|
||||
}
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
|
||||
@@ -122,19 +122,16 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
|
||||
bool result;
|
||||
std::vector<uint8_t> value;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: write directly from flash pointer
|
||||
result = this->write(this->value_.data, this->len_);
|
||||
// Static mode: copy from flash to vector
|
||||
value.assign(this->value_.data, this->value_.data + this->len_);
|
||||
} else {
|
||||
// Template mode: call function and write the vector
|
||||
std::vector<uint8_t> value = this->value_.func(x...);
|
||||
result = this->write(value);
|
||||
// Template mode: call function
|
||||
value = this->value_.func(x...);
|
||||
}
|
||||
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!result)
|
||||
if (!write(value))
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
@@ -147,15 +144,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
* errors.
|
||||
*/
|
||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||
bool write(const uint8_t *data, size_t len) {
|
||||
bool write(const std::vector<uint8_t> &value) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
||||
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
|
||||
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
|
||||
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
return false;
|
||||
@@ -163,8 +160,6 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
|
||||
@@ -49,9 +49,9 @@ void DebugComponent::dump_config() {
|
||||
}
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
this->log_partition_info_(); // Log partition information for ESP32
|
||||
#endif // USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||
this->log_partition_info_(); // Log partition information
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::loop() {
|
||||
|
||||
@@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent {
|
||||
sensor::Sensor *cpu_frequency_sensor_{nullptr};
|
||||
#endif // USE_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||
/**
|
||||
* @brief Logs information about the device's partition table.
|
||||
*
|
||||
* This function iterates through the ESP32's partition table and logs details
|
||||
* This function iterates through the partition table and logs details
|
||||
* about each partition, including its name, type, subtype, starting address,
|
||||
* and size. The information is useful for diagnosing issues related to flash
|
||||
* memory or verifying the partition configuration dynamically at runtime.
|
||||
*
|
||||
* Only available when compiled for ESP32 platforms.
|
||||
* Only available when compiled for ESP32 and ZEPHYR platforms.
|
||||
*/
|
||||
void log_partition_info_();
|
||||
#endif // USE_ESP32
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *device_info_{nullptr};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <hal/nrf_power.h>
|
||||
#include <cstdint>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
@@ -86,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() {
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
static void fa_cb(const struct flash_area *fa, void *user_data) {
|
||||
#if CONFIG_FLASH_MAP_LABELS
|
||||
const char *fa_label = flash_area_label(fa);
|
||||
|
||||
if (fa_label == nullptr) {
|
||||
fa_label = "-";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id,
|
||||
sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off,
|
||||
fa->fa_size);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2,
|
||||
(uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::log_partition_info_() {
|
||||
#if CONFIG_FLASH_MAP_LABELS
|
||||
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
|
||||
"| Label | Offset | Size");
|
||||
ESP_LOGCONFIG(TAG, "--------------------------------------------"
|
||||
"-----------------------------------------------");
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
|
||||
"| Offset | Size");
|
||||
ESP_LOGCONFIG(TAG, "-----------------------------------------"
|
||||
"------------------------------");
|
||||
#endif
|
||||
flash_area_foreach(fa_cb, nullptr);
|
||||
}
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
std::string supply = "Main supply status: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
||||
|
||||
@@ -96,10 +96,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -37,7 +37,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
|
||||
@@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
wifi::global_wifi_component->start_connecting(sta, false);
|
||||
wifi::global_wifi_component->start_connecting(sta);
|
||||
this->set_state_(improv::STATE_PROVISIONING);
|
||||
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
@@ -383,7 +383,6 @@ async def to_code(config):
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
cg.add_define("USE_ETHERNET_MANUAL_IP")
|
||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||
|
||||
# Add compile-time define for PHY types with specific code
|
||||
|
||||
@@ -550,14 +550,11 @@ void EthernetComponent::start_connect_() {
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
info.ip = this->manual_ip_->static_ip;
|
||||
info.gw = this->manual_ip_->gateway;
|
||||
info.netmask = this->manual_ip_->subnet;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
} else {
|
||||
info.ip.addr = 0;
|
||||
info.gw.addr = 0;
|
||||
info.netmask.addr = 0;
|
||||
@@ -578,7 +575,6 @@ void EthernetComponent::start_connect_() {
|
||||
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
@@ -591,9 +587,7 @@ void EthernetComponent::start_connect_() {
|
||||
d = this->manual_ip_->dns2;
|
||||
dns_setserver(1, &d);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
} else {
|
||||
err = esp_netif_dhcpc_start(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
||||
@@ -691,9 +685,7 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
|
||||
@@ -82,9 +82,7 @@ class EthernetComponent : public Component {
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
@@ -139,9 +137,7 @@ class EthernetComponent : public Component {
|
||||
uint8_t mdc_pin_{23};
|
||||
uint8_t mdio_pin_{18};
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_{};
|
||||
#endif
|
||||
uint32_t connect_begin_;
|
||||
|
||||
// Group all uint8_t types together (enums and bools)
|
||||
|
||||
@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
wifi::global_wifi_component->start_connecting(sta, false);
|
||||
wifi::global_wifi_component->start_connecting(sta);
|
||||
this->set_state_(improv::STATE_PROVISIONING);
|
||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
@@ -331,7 +331,7 @@ async def to_code(configs):
|
||||
# This must be done after all widgets are created
|
||||
for comp in helpers.lvgl_components_required:
|
||||
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
||||
if "transform_angle" in styles_used:
|
||||
if {"transform_angle", "transform_zoom"} & styles_used:
|
||||
add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
for use in helpers.lv_uses:
|
||||
add_define(f"LV_USE_{use.upper()}")
|
||||
|
||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_reg_(pin, false, iodir);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
@@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
AUTO_LOAD = ["mdns"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# High performance networking tracking infrastructure
|
||||
# Components can request high performance networking and this configures lwip and WiFi settings
|
||||
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
|
||||
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
|
||||
|
||||
network_ns = cg.esphome_ns.namespace("network")
|
||||
IPAddress = network_ns.class_("IPAddress")
|
||||
|
||||
@@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
|
||||
return IPAddress(str(ip))
|
||||
|
||||
|
||||
def require_high_performance_networking() -> None:
|
||||
"""Request high performance networking for network and WiFi.
|
||||
|
||||
Call this from components that need optimized network performance for streaming
|
||||
or high-throughput data transfer. This enables high performance mode which
|
||||
configures both lwip TCP settings and WiFi driver settings for improved
|
||||
network performance.
|
||||
|
||||
Settings applied (ESP-IDF only):
|
||||
- lwip: Larger TCP buffers, windows, and mailbox sizes
|
||||
- WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component)
|
||||
|
||||
Configuration is PSRAM-aware:
|
||||
- With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows)
|
||||
- Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows)
|
||||
|
||||
Example:
|
||||
from esphome.components import network
|
||||
|
||||
def _request_high_performance_networking(config):
|
||||
network.require_high_performance_networking()
|
||||
return config
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
...,
|
||||
_request_high_performance_networking,
|
||||
)
|
||||
"""
|
||||
# Only set up once (idempotent - multiple components can call this)
|
||||
if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False):
|
||||
CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True
|
||||
|
||||
|
||||
def has_high_performance_networking() -> bool:
|
||||
"""Check if high performance networking mode is enabled.
|
||||
|
||||
Returns True when high performance networking has been requested by a
|
||||
component or explicitly enabled in the network configuration. This indicates
|
||||
that lwip and WiFi will use optimized buffer sizes and settings.
|
||||
|
||||
This function should be called during code generation (to_code phase) by
|
||||
components that need to apply performance-related settings.
|
||||
|
||||
Returns:
|
||||
bool: True if high performance networking is enabled, False otherwise
|
||||
"""
|
||||
return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
@@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -80,6 +139,70 @@ async def to_code(config):
|
||||
cg.add_define("USE_NETWORK")
|
||||
if CORE.using_arduino and CORE.is_esp32:
|
||||
cg.add_library("Networking", None)
|
||||
|
||||
# Apply high performance networking settings
|
||||
# Config can explicitly enable/disable, or default to component-driven behavior
|
||||
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)
|
||||
component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
|
||||
|
||||
# Explicit config overrides component request
|
||||
should_enable = (
|
||||
enable_high_perf if enable_high_perf is not None else component_requested
|
||||
)
|
||||
|
||||
# Log when user explicitly disables but a component requested it
|
||||
if enable_high_perf is False and component_requested:
|
||||
_LOGGER.info(
|
||||
"High performance networking disabled by user configuration (overriding component request)"
|
||||
)
|
||||
|
||||
if CORE.is_esp32 and CORE.using_esp_idf and should_enable:
|
||||
# Check if PSRAM is guaranteed (set by psram component during final validation)
|
||||
psram_guaranteed = psram_is_guaranteed()
|
||||
|
||||
if psram_guaranteed:
|
||||
_LOGGER.info(
|
||||
"Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes"
|
||||
)
|
||||
# PSRAM is guaranteed - use aggressive settings
|
||||
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true
|
||||
# CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set
|
||||
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
|
||||
|
||||
# Enable window scaling for much larger TCP windows
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3)
|
||||
|
||||
# Large TCP buffers and windows (requires PSRAM)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000)
|
||||
|
||||
# Large mailboxes for high throughput
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512)
|
||||
|
||||
# TCP connection limits
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16)
|
||||
|
||||
# TCP optimizations
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes"
|
||||
)
|
||||
# PSRAM not guaranteed - use more conservative, but still optimized settings
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
|
||||
|
||||
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
|
||||
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
|
||||
if enable_ipv6:
|
||||
|
||||
@@ -107,7 +107,6 @@ CONF_REG0 = "reg0"
|
||||
CONF_UICR_ERASE = "uicr_erase"
|
||||
|
||||
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
|
||||
DEFAULT_VOLTAGE_LEVEL = "default"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
_detect_bootloader,
|
||||
@@ -124,12 +123,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_REG0): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VOLTAGE): cv.Any(
|
||||
cv.All(
|
||||
cv.voltage,
|
||||
cv.one_of(*VOLTAGE_LEVELS, float=True),
|
||||
),
|
||||
cv.one_of(*[DEFAULT_VOLTAGE_LEVEL], lower=True),
|
||||
cv.Required(CONF_VOLTAGE): cv.All(
|
||||
cv.voltage,
|
||||
cv.one_of(*VOLTAGE_LEVELS, float=True),
|
||||
),
|
||||
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
|
||||
}
|
||||
@@ -202,9 +198,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(_dfu_to_code, dfu_config)
|
||||
|
||||
if reg0_config := config.get(CONF_REG0):
|
||||
value = 7 # DEFAULT_VOLTAGE_LEVEL
|
||||
if reg0_config[CONF_VOLTAGE] in VOLTAGE_LEVELS:
|
||||
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
|
||||
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
|
||||
cg.add_define("USE_NRF52_REG0_VOUT", value)
|
||||
if reg0_config[CONF_UICR_ERASE]:
|
||||
cg.add_define("USE_NRF52_UICR_ERASE")
|
||||
|
||||
@@ -69,9 +69,20 @@ static StatusFlags fix_bootloader() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
static StatusFlags set_uicr() {
|
||||
StatusFlags status = StatusFlags::OK;
|
||||
status |= set_regout0();
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
if (BOOTLOADER_VERSION_REGISTER <= 0x902) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER);
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
status |= set_regout0();
|
||||
}
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
status |= fix_bootloader();
|
||||
#endif
|
||||
|
||||
@@ -35,6 +35,9 @@ DOMAIN = "psram"
|
||||
|
||||
DEPENDENCIES = [PLATFORM_ESP32]
|
||||
|
||||
# PSRAM availability tracking for cross-component coordination
|
||||
KEY_PSRAM_GUARANTEED = "psram_guaranteed"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
psram_ns = cg.esphome_ns.namespace(DOMAIN)
|
||||
@@ -71,6 +74,23 @@ def supported() -> bool:
|
||||
return variant in SPIRAM_MODES
|
||||
|
||||
|
||||
def is_guaranteed() -> bool:
|
||||
"""Check if PSRAM is guaranteed to be available.
|
||||
|
||||
Returns True when PSRAM is configured with both 'disabled: false' and
|
||||
'ignore_not_found: false', meaning the device will fail to boot if PSRAM
|
||||
is not found. This ensures safe use of high buffer configurations that
|
||||
depend on PSRAM.
|
||||
|
||||
This function should be called during code generation (to_code phase) by
|
||||
components that need to know PSRAM availability for configuration decisions.
|
||||
|
||||
Returns:
|
||||
bool: True if PSRAM is guaranteed, False otherwise
|
||||
"""
|
||||
return CORE.data.get(KEY_PSRAM_GUARANTEED, False)
|
||||
|
||||
|
||||
def validate_psram_mode(config):
|
||||
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
|
||||
if config[CONF_SPEED] == "120MHZ":
|
||||
@@ -131,7 +151,22 @@ def get_config_schema(config):
|
||||
|
||||
CONFIG_SCHEMA = get_config_schema
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_psram_mode
|
||||
|
||||
def _store_psram_guaranteed(config):
|
||||
"""Store PSRAM guaranteed status in CORE.data for other components.
|
||||
|
||||
PSRAM is "guaranteed" when it will fail if not found, ensuring safe use
|
||||
of high buffer configurations in network/wifi components.
|
||||
|
||||
Called during final validation to ensure the flag is available
|
||||
before any to_code() functions run.
|
||||
"""
|
||||
psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND]
|
||||
CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -77,21 +77,23 @@ class Select : public EntityBase {
|
||||
|
||||
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control().
|
||||
* Override this to work directly with indices and avoid string conversions.
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
protected:
|
||||
friend class SelectCall;
|
||||
|
||||
size_t active_index_{0};
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
* Overriding this index-based version is PREFERRED as it avoids string conversions.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control(const std::string&).
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
/** Set the value of the select, this is a virtual method that each select integration can implement.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
|
||||
@@ -74,9 +74,9 @@ StateClass Sensor::get_state_class() {
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
this->callbacks_.call_first(this->raw_count_, state);
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
|
||||
|
||||
@@ -87,12 +87,12 @@ void Sensor::publish_state(float state) {
|
||||
}
|
||||
}
|
||||
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->callbacks_.add_second(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
|
||||
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
|
||||
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
|
||||
if (!this->raw_callback_) {
|
||||
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
|
||||
}
|
||||
this->raw_callback_->add(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::add_filter(Filter *filter) {
|
||||
@@ -132,10 +132,7 @@ void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->state = state;
|
||||
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
|
||||
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
|
||||
|
||||
// Call filtered callbacks (after filters)
|
||||
this->callbacks_.call_second(this->raw_count_, state);
|
||||
|
||||
this->callback_.call(state);
|
||||
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
|
||||
@@ -124,7 +124,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
|
||||
void internal_send_state_to_frontend(float state);
|
||||
|
||||
protected:
|
||||
PartitionedCallbackManager<void(float)> callbacks_;
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
|
||||
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
@@ -139,8 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
|
||||
uint8_t force_update : 1;
|
||||
uint8_t reserved : 5; // Reserved for future use
|
||||
} sensor_flags_{};
|
||||
|
||||
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
|
||||
};
|
||||
|
||||
} // namespace sensor
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
from esphome import automation, external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, media_player, psram, speaker
|
||||
from esphome.components import audio, esp32, media_player, network, psram, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
@@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
|
||||
DOMAIN = "media_player"
|
||||
@@ -280,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _request_high_performance_networking(config):
|
||||
"""Request high performance networking for streaming media.
|
||||
|
||||
Speaker media player streams audio data, so it always benefits from
|
||||
optimized WiFi and lwip settings regardless of codec support.
|
||||
Called during config validation to ensure flags are set before to_code().
|
||||
"""
|
||||
network.require_high_performance_networking()
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_player.media_player_schema(SpeakerMediaPlayer).extend(
|
||||
{
|
||||
@@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.only_with_esp_idf,
|
||||
_validate_repeated_speaker,
|
||||
_request_high_performance_networking,
|
||||
)
|
||||
|
||||
|
||||
@@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]:
|
||||
# Compile all supported audio codecs and optimize the wifi settings
|
||||
|
||||
# Compile all supported audio codecs
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT", True)
|
||||
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
|
||||
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
|
||||
|
||||
# Allocate wifi buffers in PSRAM
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
||||
|
||||
var = await media_player.new_media_player(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
|
||||
|
||||
void TextSensor::publish_state(const std::string &state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
this->callbacks_.call_first(this->raw_count_, state);
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
|
||||
|
||||
@@ -70,11 +70,13 @@ void TextSensor::clear_filters() {
|
||||
}
|
||||
|
||||
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
|
||||
this->callbacks_.add_second(std::move(callback));
|
||||
this->callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
|
||||
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
|
||||
if (!this->raw_callback_) {
|
||||
this->raw_callback_ = make_unique<CallbackManager<void(std::string)>>();
|
||||
}
|
||||
this->raw_callback_->add(std::move(callback));
|
||||
}
|
||||
|
||||
std::string TextSensor::get_state() const { return this->state; }
|
||||
@@ -83,10 +85,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
|
||||
this->state = state;
|
||||
this->set_has_state(true);
|
||||
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
|
||||
|
||||
// Call filtered callbacks (after filters)
|
||||
this->callbacks_.call_second(this->raw_count_, state);
|
||||
|
||||
this->callback_.call(state);
|
||||
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_text_sensor_update(this);
|
||||
#endif
|
||||
|
||||
@@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
void internal_send_state_to_frontend(const std::string &state);
|
||||
|
||||
protected:
|
||||
PartitionedCallbackManager<void(std::string)> callbacks_;
|
||||
std::unique_ptr<CallbackManager<void(std::string)>>
|
||||
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
|
||||
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
|
||||
|
||||
Filter *filter_list_{nullptr}; ///< Store all active filters.
|
||||
|
||||
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
|
||||
};
|
||||
|
||||
} // namespace text_sensor
|
||||
|
||||
@@ -353,9 +353,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
void AsyncResponseStream::print(float value) {
|
||||
// Use stack buffer to avoid temporary string allocation
|
||||
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
|
||||
constexpr size_t float_buf_size = 32;
|
||||
char buf[float_buf_size];
|
||||
int len = snprintf(buf, float_buf_size, "%f", value);
|
||||
char buf[32];
|
||||
int len = snprintf(buf, sizeof(buf), "%f", value);
|
||||
this->content_.append(buf, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_USE_PSRAM
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.network import (
|
||||
has_high_performance_networking,
|
||||
ip_address_literal,
|
||||
)
|
||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import only_with_esp_idf
|
||||
@@ -56,6 +60,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["network"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
|
||||
CONF_SAVE = "save"
|
||||
CONF_MIN_AUTH_MODE = "min_auth_mode"
|
||||
@@ -496,6 +502,56 @@ async def to_code(config):
|
||||
|
||||
if config.get(CONF_USE_PSRAM):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
||||
|
||||
# Apply high performance WiFi settings if high performance networking is enabled
|
||||
if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking():
|
||||
# Check if PSRAM is guaranteed (set by psram component during final validation)
|
||||
psram_guaranteed = psram_is_guaranteed()
|
||||
|
||||
# Always allocate WiFi buffers in PSRAM if available
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
||||
|
||||
if psram_guaranteed:
|
||||
_LOGGER.info(
|
||||
"Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers"
|
||||
)
|
||||
# PSRAM is guaranteed - use aggressive settings
|
||||
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component
|
||||
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
|
||||
|
||||
# Large dynamic RX buffers (requires PSRAM)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512)
|
||||
|
||||
# Static TX buffers for better performance
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8)
|
||||
|
||||
# AMPDU settings optimized for PSRAM
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers"
|
||||
)
|
||||
# PSRAM not guaranteed - use more conservative, but still optimized settings
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
|
||||
# Standard buffer counts
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
|
||||
|
||||
# Standard AMPDU settings
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
|
||||
|
||||
cg.add_define("USE_WIFI")
|
||||
|
||||
# must register before OTA safe mode check
|
||||
|
||||
@@ -197,6 +197,10 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
|
||||
// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
|
||||
static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
|
||||
|
||||
/// Cooldown duration in milliseconds after adapter restart or repeated failures
|
||||
/// Allows WiFi hardware to stabilize before next connection attempt
|
||||
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000;
|
||||
|
||||
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
@@ -275,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
|
||||
return static_cast<int8_t>(i);
|
||||
}
|
||||
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str());
|
||||
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
|
||||
}
|
||||
// No hidden SSIDs found
|
||||
return -1;
|
||||
@@ -289,7 +293,7 @@ void WiFiComponent::start_initial_connection_() {
|
||||
this->selected_sta_index_ = 0;
|
||||
this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, false);
|
||||
this->start_connecting(params);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Starting scan");
|
||||
this->start_scanning();
|
||||
@@ -371,13 +375,13 @@ void WiFiComponent::start() {
|
||||
// Without saved data, try first configured network or use normal flow
|
||||
if (loaded_fast_connect) {
|
||||
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
|
||||
this->start_connecting(params, false);
|
||||
this->start_connecting(params);
|
||||
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
|
||||
// No saved data, but have configured networks - try first non-hidden network
|
||||
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
|
||||
this->selected_sta_index_ = 0;
|
||||
params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, false);
|
||||
this->start_connecting(params);
|
||||
} else {
|
||||
// No saved data and (no networks OR first is hidden) - use normal flow
|
||||
this->start_initial_connection_();
|
||||
@@ -417,6 +421,7 @@ void WiFiComponent::restart_adapter() {
|
||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
this->error_from_callback_ = false;
|
||||
}
|
||||
|
||||
void WiFiComponent::loop() {
|
||||
@@ -436,7 +441,7 @@ void WiFiComponent::loop() {
|
||||
switch (this->state_) {
|
||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||
if (millis() - this->action_started_ > 5000) {
|
||||
if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) {
|
||||
// After cooldown we either restarted the adapter because of
|
||||
// a failure, or something tried to connect over and over
|
||||
// so we entered cooldown. In both cases we call
|
||||
@@ -450,8 +455,7 @@ void WiFiComponent::loop() {
|
||||
this->check_scanning_finished();
|
||||
break;
|
||||
}
|
||||
case WIFI_COMPONENT_STATE_STA_CONNECTING:
|
||||
case WIFI_COMPONENT_STATE_STA_CONNECTING_2: {
|
||||
case WIFI_COMPONENT_STATE_STA_CONNECTING: {
|
||||
this->status_set_warning(LOG_STR("associating to network"));
|
||||
this->check_connecting_finished();
|
||||
break;
|
||||
@@ -662,7 +666,7 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
||||
this->set_sta(sta);
|
||||
}
|
||||
|
||||
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
||||
void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
// Log connection attempt at INFO level with priority
|
||||
std::string bssid_formatted;
|
||||
int8_t priority = 0;
|
||||
@@ -729,14 +733,11 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
||||
|
||||
if (!this->wifi_sta_connect_(ap)) {
|
||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!two) {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
// Enter cooldown to allow WiFi hardware to stabilize
|
||||
// (immediate failure suggests hardware not ready, different from connection timeout)
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
} else {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
}
|
||||
this->action_started_ = millis();
|
||||
}
|
||||
@@ -1017,7 +1018,7 @@ void WiFiComponent::check_scanning_finished() {
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
// Ensure we're in SCAN_CONNECTING phase when connecting with scan results
|
||||
// (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
|
||||
this->start_connecting(params, false);
|
||||
this->start_connecting(params);
|
||||
}
|
||||
|
||||
void WiFiComponent::dump_config() {
|
||||
@@ -1086,18 +1087,12 @@ void WiFiComponent::check_connecting_finished() {
|
||||
uint32_t now = millis();
|
||||
if (now - this->action_started_ > 30000) {
|
||||
ESP_LOGW(TAG, "Connection timeout");
|
||||
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
|
||||
// since we know the connection attempt has failed
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->error_from_callback_) {
|
||||
ESP_LOGW(TAG, "Connecting to network failed (callback)");
|
||||
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
|
||||
// since we know the connection attempt is finished
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
@@ -1106,9 +1101,6 @@ void WiFiComponent::check_connecting_finished() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
|
||||
// since we know the connection attempt is finished
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||
if (status == WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND) {
|
||||
ESP_LOGW(TAG, "Network no longer found");
|
||||
this->retry_connect();
|
||||
@@ -1398,7 +1390,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
|
||||
this->set_sta_priority(failed_bssid.value(), new_priority);
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(),
|
||||
format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
|
||||
|
||||
@@ -1492,27 +1483,14 @@ void WiFiComponent::retry_connect() {
|
||||
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) {
|
||||
yield();
|
||||
// Check if we have a valid target before building params
|
||||
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||
// In that case, skip connection and let next wifi_loop() handle phase transition
|
||||
if (this->selected_sta_index_ >= 0) {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, true);
|
||||
}
|
||||
return;
|
||||
yield();
|
||||
// Check if we have a valid target before building params
|
||||
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||
// In that case, skip connection and let next wifi_loop() handle phase transition
|
||||
if (this->selected_sta_index_ >= 0) {
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params);
|
||||
}
|
||||
|
||||
// If we can't progress forward its likely because scanning failed
|
||||
// or the stack is in a bad state after restart so we cooldown first
|
||||
// and once it finishes, cooldown will call check_connecting_finished()
|
||||
// which will progress the state machine
|
||||
ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_,
|
||||
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
}
|
||||
|
||||
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
@@ -74,12 +74,6 @@ enum WiFiComponentState : uint8_t {
|
||||
WIFI_COMPONENT_STATE_STA_SCANNING,
|
||||
/** WiFi is in STA(+AP) mode and currently connecting to an AP. */
|
||||
WIFI_COMPONENT_STATE_STA_CONNECTING,
|
||||
/** WiFi is in STA(+AP) mode and currently connecting to an AP a second time.
|
||||
*
|
||||
* This is required because for some reason ESPs don't like to connect to WiFi APs directly after
|
||||
* a scan.
|
||||
* */
|
||||
WIFI_COMPONENT_STATE_STA_CONNECTING_2,
|
||||
/** WiFi is in STA(+AP) mode and successfully connected. */
|
||||
WIFI_COMPONENT_STATE_STA_CONNECTED,
|
||||
/** WiFi is in AP-only mode and internal AP is already enabled. */
|
||||
@@ -275,7 +269,9 @@ class WiFiComponent : public Component {
|
||||
bool is_disabled();
|
||||
void start_scanning();
|
||||
void check_scanning_finished();
|
||||
void start_connecting(const WiFiAP &ap, bool two);
|
||||
void start_connecting(const WiFiAP &ap);
|
||||
// Backward compatibility overload - ignores 'two' parameter
|
||||
void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); }
|
||||
|
||||
void check_connecting_finished();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.11.0-dev"
|
||||
__version__ = "2025.11.0b1"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -710,15 +710,6 @@ class EsphomeCore:
|
||||
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
|
||||
return self.relative_build_path(".piolibdeps", *path)
|
||||
|
||||
@property
|
||||
def platformio_cache_dir(self) -> str:
|
||||
"""Get the PlatformIO cache directory path."""
|
||||
# Check if running in Docker/HA addon with custom cache dir
|
||||
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
|
||||
return cache_dir
|
||||
# Default PlatformIO cache location
|
||||
return os.path.expanduser("~/.platformio/.cache")
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
if self.is_libretiny:
|
||||
|
||||
@@ -216,7 +216,6 @@
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||
#define USE_ETHERNET
|
||||
#define USE_ETHERNET_KSZ8081
|
||||
#define USE_ETHERNET_MANUAL_IP
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
@@ -414,8 +414,10 @@ int8_t step_to_accuracy_decimals(float step) {
|
||||
return str.length() - dot_pos - 1;
|
||||
}
|
||||
|
||||
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
|
||||
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Helper function to find the index of a base64 character in the lookup table.
|
||||
// Returns the character's position (0-63) if found, or 0 if not found.
|
||||
@@ -425,8 +427,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
|
||||
// stops processing at the first invalid character due to the is_base64() check in its
|
||||
// while loop condition, making this edge case harmless in practice.
|
||||
static inline uint8_t base64_find_char(char c) {
|
||||
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
|
||||
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
|
||||
const char *pos = strchr(BASE64_CHARS, c);
|
||||
return pos ? (pos - BASE64_CHARS) : 0;
|
||||
}
|
||||
|
||||
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
|
||||
|
||||
@@ -145,9 +145,6 @@ template<typename T, size_t N> class StaticVector {
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
|
||||
// Direct access to size counter for efficient in-place construction
|
||||
size_t &count() { return count_; }
|
||||
|
||||
T &operator[](size_t i) { return data_[i]; }
|
||||
const T &operator[](size_t i) const { return data_[i]; }
|
||||
|
||||
@@ -872,73 +869,6 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
std::vector<std::function<void(Ts...)>> callbacks_;
|
||||
};
|
||||
|
||||
template<typename... X> class PartitionedCallbackManager;
|
||||
|
||||
/** Helper class for callbacks partitioned into two sections.
|
||||
*
|
||||
* Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1]
|
||||
* The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment).
|
||||
*
|
||||
* Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms).
|
||||
* The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste.
|
||||
*
|
||||
* Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value)
|
||||
* is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count
|
||||
* internally, saving memory per instance.
|
||||
*
|
||||
* @tparam Ts The arguments for the callbacks, wrapped in void().
|
||||
*/
|
||||
template<typename... Ts> class PartitionedCallbackManager<void(Ts...)> {
|
||||
public:
|
||||
/// Add a callback to the first partition.
|
||||
void add_first(std::function<void(Ts...)> &&callback, uint8_t *first_count) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
|
||||
}
|
||||
|
||||
// Add to first partition: append then rotate into position
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
// Avoid potential underflow: rewrite comparison to not subtract from size()
|
||||
if (*first_count + 1 < this->callbacks_->size()) {
|
||||
// Use std::rotate to maintain registration order in second partition
|
||||
std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end());
|
||||
}
|
||||
(*first_count)++;
|
||||
}
|
||||
|
||||
/// Add a callback to the second partition.
|
||||
void add_second(std::function<void(Ts...)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
|
||||
}
|
||||
|
||||
// Add to second partition: just append (already at end after first partition)
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
/// Call all callbacks in the first partition.
|
||||
void call_first(uint8_t first_count, Ts... args) {
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < first_count; i++) {
|
||||
(*this->callbacks_)[i](args...);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call all callbacks in the second partition.
|
||||
void call_second(uint8_t first_count, Ts... args) {
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = first_count; i < this->callbacks_->size(); i++) {
|
||||
(*this->callbacks_)[i](args...);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1]
|
||||
std::unique_ptr<std::vector<std::function<void(Ts...)>>> callbacks_;
|
||||
};
|
||||
|
||||
/// Helper class to deduplicate items in a series of values.
|
||||
template<typename T> class Deduplicator {
|
||||
public:
|
||||
|
||||
@@ -94,9 +94,10 @@ class Scheduler {
|
||||
} name_;
|
||||
uint32_t interval;
|
||||
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
|
||||
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
|
||||
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
|
||||
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
|
||||
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
|
||||
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
|
||||
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
|
||||
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
|
||||
// even when devices run for months. Split into two fields for better memory
|
||||
// alignment on 32-bit systems.
|
||||
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
|
||||
|
||||
@@ -145,16 +145,7 @@ def run_compile(config, verbose):
|
||||
args = []
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
|
||||
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
|
||||
result = run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
# Run memory analysis if enabled
|
||||
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
|
||||
try:
|
||||
analyze_memory_usage(config)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("Failed to analyze memory usage: %s", e)
|
||||
|
||||
return result
|
||||
return run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
|
||||
def _run_idedata(config):
|
||||
@@ -403,74 +394,3 @@ class IDEData:
|
||||
if path.endswith(".exe")
|
||||
else f"{path[:-3]}readelf"
|
||||
)
|
||||
|
||||
|
||||
def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
"""Analyze memory usage by component after compilation."""
|
||||
# Lazy import to avoid overhead when not needed
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.helpers import get_esphome_components
|
||||
|
||||
idedata = get_idedata(config)
|
||||
|
||||
# Get paths to tools
|
||||
elf_path = idedata.firmware_elf_path
|
||||
objdump_path = idedata.objdump_path
|
||||
readelf_path = idedata.readelf_path
|
||||
|
||||
# Debug logging
|
||||
_LOGGER.debug("ELF path from idedata: %s", elf_path)
|
||||
|
||||
# Check if file exists
|
||||
if not Path(elf_path).exists():
|
||||
# Try alternate path
|
||||
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
|
||||
if alt_path.exists():
|
||||
elf_path = str(alt_path)
|
||||
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
|
||||
else:
|
||||
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
|
||||
return
|
||||
|
||||
# Extract external components from config
|
||||
external_components = set()
|
||||
|
||||
# Get the list of built-in ESPHome components
|
||||
builtin_components = get_esphome_components()
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
NON_COMPONENT_KEYS = {
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"<<",
|
||||
}
|
||||
|
||||
# Check all top-level keys in config
|
||||
for key in config:
|
||||
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
|
||||
# This is an external component
|
||||
external_components.add(key)
|
||||
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Create analyzer and run analysis
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
elf_path, objdump_path, readelf_path, external_components
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and print report
|
||||
report = analyzer.generate_report()
|
||||
_LOGGER.info("\n%s", report)
|
||||
|
||||
# Optionally save to file
|
||||
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
|
||||
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
|
||||
if report_file.suffix == ".json":
|
||||
report_file.write_text(analyzer.to_json())
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
else:
|
||||
report_file.write_text(report)
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
|
||||
@@ -66,6 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
# Stateless lambda optimization: empty capture list allows function pointer conversion
|
||||
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
|
||||
assert 'return std::string{"Hello"};' in main_cpp
|
||||
|
||||
1
tests/components/debug/test.nrf52-xiao-ble.yaml
Normal file
1
tests/components/debug/test.nrf52-xiao-ble.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -1 +1,4 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
|
||||
@@ -6,4 +6,4 @@ nrf52:
|
||||
mode:
|
||||
output: true
|
||||
reg0:
|
||||
voltage: default
|
||||
voltage: 1.8V
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
esphome:
|
||||
name: test-user-services-union
|
||||
friendly_name: Test User Services Union Storage
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: "test"
|
||||
password: "password"
|
||||
|
||||
api:
|
||||
actions:
|
||||
# Test service with no arguments
|
||||
- action: test_no_args
|
||||
then:
|
||||
- logger.log: "No args service called"
|
||||
|
||||
# Test service with one argument
|
||||
- action: test_one_arg
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "One arg service: %d"
|
||||
args: [value]
|
||||
|
||||
# Test service with multiple arguments of different types
|
||||
- action: test_multi_args
|
||||
variables:
|
||||
int_val: int
|
||||
float_val: float
|
||||
str_val: string
|
||||
bool_val: bool
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Multi args: %d, %.2f, %s, %d"
|
||||
args: [int_val, float_val, str_val.c_str(), bool_val]
|
||||
|
||||
# Test service with max typical arguments
|
||||
- action: test_many_args
|
||||
variables:
|
||||
arg1: int
|
||||
arg2: int
|
||||
arg3: int
|
||||
arg4: string
|
||||
arg5: float
|
||||
then:
|
||||
- logger.log: "Many args service called"
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: "Test Binary Sensor"
|
||||
id: test_sensor
|
||||
@@ -355,7 +355,6 @@ def test_clean_build(
|
||||
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
|
||||
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
|
||||
mock_core.relative_build_path.return_value = dependencies_lock
|
||||
mock_core.platformio_cache_dir = str(platformio_cache_dir)
|
||||
|
||||
# Verify all exist before
|
||||
assert pioenvs_dir.exists()
|
||||
|
||||
Reference in New Issue
Block a user