mirror of
https://github.com/esphome/esphome.git
synced 2025-10-23 12:13:49 +01:00
Merge branch 'dev' into ListEntitiesServicesArgument_FixedVector
This commit is contained in:
@@ -221,6 +221,104 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
|
||||
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
|
||||
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
|
||||
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
|
||||
|
||||
**STL Container Guidelines:**
|
||||
|
||||
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
|
||||
|
||||
1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code
|
||||
std::vector<int> values;
|
||||
|
||||
// Good - no dynamic allocation
|
||||
std::array<int, MAX_VALUES> values;
|
||||
```
|
||||
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
|
||||
|
||||
**For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.
|
||||
|
||||
> **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.
|
||||
|
||||
```cpp
|
||||
// Bad - STL overhead for simple byte buffer
|
||||
std::vector<uint8_t> buffer;
|
||||
buffer.resize(256);
|
||||
|
||||
// Good - minimal overhead, single allocation
|
||||
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
|
||||
// Or if size is constant:
|
||||
std::array<uint8_t, 256> buffer;
|
||||
```
|
||||
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
std::vector<ServiceRecord> services;
|
||||
services.reserve(5); // Still includes reallocation machinery
|
||||
|
||||
// Good - compile-time fixed size, stack allocated, no reallocation machinery
|
||||
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
|
||||
services.push_back(record1); // Tracks count but all slots allocated
|
||||
```
|
||||
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
||||
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
||||
|
||||
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
std::vector<TxtRecord> txt_records;
|
||||
txt_records.reserve(5); // Still includes reallocation machinery
|
||||
|
||||
// Good - runtime size, single allocation, no reallocation machinery
|
||||
FixedVector<TxtRecord> txt_records;
|
||||
txt_records.init(record_count); // Initialize with exact size at runtime
|
||||
```
|
||||
**Benefits:**
|
||||
- Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
|
||||
- Single allocation, no upper bound needed
|
||||
- No reallocation overhead
|
||||
- Compatible with protobuf code generation when using `[(fixed_vector) = true]` option
|
||||
|
||||
4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
|
||||
```cpp
|
||||
// Bad - 2KB+ overhead for red-black tree/hash table
|
||||
std::map<std::string, int> small_lookup;
|
||||
std::unordered_map<int, std::string> tiny_map;
|
||||
|
||||
// Good - simple struct with linear search (std::vector is fine)
|
||||
struct LookupEntry {
|
||||
const char *key;
|
||||
int value;
|
||||
};
|
||||
std::vector<LookupEntry> small_lookup = {
|
||||
{"key1", 10},
|
||||
{"key2", 20},
|
||||
{"key3", 30},
|
||||
};
|
||||
// Or std::array if size is compile-time constant:
|
||||
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
|
||||
```
|
||||
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
||||
|
||||
5. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
- Red-black tree code (`rb_tree`, `_Rb_tree`)
|
||||
- Hash table infrastructure (`unordered_map`, `hash`)
|
||||
|
||||
**When to optimize:**
|
||||
- Core components (API, network, logger)
|
||||
- Widely-used components (mdns, wifi, ble)
|
||||
- Components causing flash size complaints
|
||||
|
||||
**When not to optimize:**
|
||||
- Single-use niche components
|
||||
- Code where readability matters more than bytes
|
||||
- Already using appropriate containers
|
||||
|
||||
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
|
||||
|
||||
**Bad Pattern (Module-Level Globals):**
|
||||
|
@@ -776,9 +776,9 @@ message HomeassistantActionRequest {
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
|
||||
string service = 1;
|
||||
repeated HomeassistantServiceMap data = 2;
|
||||
repeated HomeassistantServiceMap data_template = 3;
|
||||
repeated HomeassistantServiceMap variables = 4;
|
||||
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
|
||||
bool is_event = 5;
|
||||
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
|
@@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
#endif
|
||||
StringRef service_ref_{};
|
||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||
std::vector<HomeassistantServiceMap> data{};
|
||||
std::vector<HomeassistantServiceMap> data_template{};
|
||||
std::vector<HomeassistantServiceMap> variables{};
|
||||
FixedVector<HomeassistantServiceMap> data{};
|
||||
FixedVector<HomeassistantServiceMap> data_template{};
|
||||
FixedVector<HomeassistantServiceMap> variables{};
|
||||
bool is_event{false};
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
uint32_t call_id{0};
|
||||
|
@@ -201,9 +201,9 @@ class CustomAPIDevice {
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
}
|
||||
@@ -244,9 +244,9 @@ class CustomAPIDevice {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.is_event = true;
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
}
|
||||
|
@@ -127,24 +127,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
std::string service_value = this->service_.value(x...);
|
||||
resp.set_service(StringRef(service_value));
|
||||
resp.is_event = this->flags_.is_event;
|
||||
for (auto &it : this->data_) {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
for (auto &it : this->data_template_) {
|
||||
resp.data_template.emplace_back();
|
||||
auto &kv = resp.data_template.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
for (auto &it : this->variables_) {
|
||||
resp.variables.emplace_back();
|
||||
auto &kv = resp.variables.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
this->populate_service_map(resp.data, this->data_, x...);
|
||||
this->populate_service_map(resp.data_template, this->data_template_, x...);
|
||||
this->populate_service_map(resp.variables, this->variables_, x...);
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
if (this->flags_.wants_status) {
|
||||
@@ -189,6 +174,16 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
protected:
|
||||
template<typename VectorType, typename SourceType>
|
||||
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
|
||||
dest.init(source.size());
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
TemplatableStringValue<Ts...> service_{};
|
||||
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
||||
|
@@ -90,13 +90,12 @@ void HomeassistantNumber::control(float value) {
|
||||
api::HomeassistantActionRequest resp;
|
||||
resp.set_service(SERVICE_NAME);
|
||||
|
||||
resp.data.emplace_back();
|
||||
auto &entity_id = resp.data.back();
|
||||
resp.data.init(2);
|
||||
auto &entity_id = resp.data.emplace_back();
|
||||
entity_id.set_key(ENTITY_ID_KEY);
|
||||
entity_id.value = this->entity_id_;
|
||||
|
||||
resp.data.emplace_back();
|
||||
auto &entity_value = resp.data.back();
|
||||
auto &entity_value = resp.data.emplace_back();
|
||||
entity_value.set_key(VALUE_KEY);
|
||||
entity_value.value = to_string(value);
|
||||
|
||||
|
@@ -51,8 +51,8 @@ void HomeassistantSwitch::write_state(bool state) {
|
||||
resp.set_service(SERVICE_OFF);
|
||||
}
|
||||
|
||||
resp.data.emplace_back();
|
||||
auto &entity_id_kv = resp.data.back();
|
||||
resp.data.init(1);
|
||||
auto &entity_id_kv = resp.data.emplace_back();
|
||||
entity_id_kv.set_key(ENTITY_ID_KEY);
|
||||
entity_id_kv.value = this->entity_id_;
|
||||
|
||||
|
@@ -218,7 +218,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
}
|
||||
case improv::GET_WIFI_NETWORKS: {
|
||||
std::vector<std::string> networks;
|
||||
auto results = wifi::global_wifi_component->get_scan_result();
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
|
@@ -177,9 +177,10 @@ void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ =
|
||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
|
||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||
const std::vector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
|
||||
this->effects_.reserve(this->effects_.size() + effects.size());
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_.init(effects.size());
|
||||
for (auto *effect : effects) {
|
||||
this->effects_.push_back(effect);
|
||||
}
|
||||
|
@@ -11,8 +11,9 @@
|
||||
#include "light_traits.h"
|
||||
#include "light_transformer.h"
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <strings.h>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
@@ -159,7 +160,7 @@ class LightState : public EntityBase, public Component {
|
||||
bool supports_effects();
|
||||
|
||||
/// Get all effects for this light state.
|
||||
const std::vector<LightEffect *> &get_effects() const;
|
||||
const FixedVector<LightEffect *> &get_effects() const;
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::vector<LightEffect *> &effects);
|
||||
@@ -260,7 +261,7 @@ class LightState : public EntityBase, public Component {
|
||||
/// The currently active transformer for this light (transition/flash).
|
||||
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
||||
/// List of effects for this light.
|
||||
std::vector<LightEffect *> effects_;
|
||||
FixedVector<LightEffect *> effects_;
|
||||
/// Object used to store the persisted values of the light.
|
||||
ESPPreferenceObject rtc_;
|
||||
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
||||
|
@@ -83,7 +83,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
#endif
|
||||
|
||||
auto &txt_records = service.txt_records;
|
||||
txt_records.reserve(txt_count);
|
||||
txt_records.init(txt_count);
|
||||
|
||||
if (!friendly_name_empty) {
|
||||
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
|
||||
@@ -171,12 +171,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
|
||||
fallback_service.proto = MDNS_STR(SERVICE_TCP);
|
||||
fallback_service.port = USE_WEBSERVER_PORT;
|
||||
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
// Copy to member variable if storage is enabled (verbose logging, OpenThread, or extra services)
|
||||
this->services_ = services;
|
||||
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@@ -38,7 +38,7 @@ struct MDNSService {
|
||||
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
|
||||
const MDNSString *proto;
|
||||
TemplatableValue<uint16_t> port;
|
||||
std::vector<MDNSTXTRecord> txt_records;
|
||||
FixedVector<MDNSTXTRecord> txt_records;
|
||||
};
|
||||
|
||||
class MDNSComponent : public Component {
|
||||
|
@@ -12,8 +12,13 @@ namespace mdns {
|
||||
static const char *const TAG = "mdns";
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
esp_err_t err = mdns_init();
|
||||
if (err != ESP_OK) {
|
||||
|
@@ -12,8 +12,13 @@ namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
|
||||
|
@@ -12,8 +12,13 @@ namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
|
||||
|
@@ -12,8 +12,13 @@ namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
|
||||
|
@@ -56,50 +56,41 @@ DriverChip(
|
||||
"WAVESHARE-P4-86-PANEL",
|
||||
height=720,
|
||||
width=720,
|
||||
hsync_back_porch=80,
|
||||
hsync_back_porch=50,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=80,
|
||||
vsync_back_porch=12,
|
||||
hsync_front_porch=50,
|
||||
vsync_back_porch=20,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="46MHz",
|
||||
lane_bit_rate="1Gbps",
|
||||
vsync_front_porch=20,
|
||||
pclk_frequency="38MHz",
|
||||
lane_bit_rate="480Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=27,
|
||||
initsequence=[
|
||||
(0xB9, 0xF1, 0x12, 0x83),
|
||||
(
|
||||
0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00,
|
||||
0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37,
|
||||
),
|
||||
(0xB8, 0x25, 0x22, 0xF0, 0x63),
|
||||
(0xBF, 0x02, 0x11, 0x00),
|
||||
(0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80),
|
||||
(0xB2, 0x3C, 0x12, 0x30),
|
||||
(0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
|
||||
(0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30),
|
||||
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,),
|
||||
(0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33),
|
||||
(0xB4, 0x80),
|
||||
(0xB5, 0x0A, 0x0A),
|
||||
(0xB6, 0xB2, 0xB2),
|
||||
(
|
||||
0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80,
|
||||
0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48,
|
||||
0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57,
|
||||
0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
),
|
||||
(
|
||||
0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31,
|
||||
0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64,
|
||||
0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
),
|
||||
(
|
||||
0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14,
|
||||
0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13,
|
||||
0x15, 0x14, 0x15, 0x10, 0x17,
|
||||
),
|
||||
(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),
|
||||
(0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
|
||||
(0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77),
|
||||
(0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF),
|
||||
(0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09),
|
||||
(0xC8, 0x10, 0x40, 0x1E, 0x02),
|
||||
(0xCC, 0x0B),
|
||||
(0xE0, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16),
|
||||
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x0B, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10),
|
||||
(0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
|
||||
(0xEF, 0xFF, 0xFF, 0x01),
|
||||
(0x11, 0x00),
|
||||
(0x29, 0x00),
|
||||
],
|
||||
)
|
||||
|
@@ -380,24 +380,25 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
if (this->on_connect_) {
|
||||
this->on_connect_(rsp);
|
||||
}
|
||||
this->sessions_.insert(rsp);
|
||||
this->sessions_.push_back(rsp);
|
||||
}
|
||||
|
||||
void AsyncEventSource::loop() {
|
||||
// Clean up dead sessions safely
|
||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||
// and the main loop handles the actual cleanup to avoid race conditions
|
||||
auto it = this->sessions_.begin();
|
||||
while (it != this->sessions_.end()) {
|
||||
auto *ses = *it;
|
||||
for (size_t i = 0; i < this->sessions_.size();) {
|
||||
auto *ses = this->sessions_[i];
|
||||
// If the session has a dead socket (marked by destroy callback)
|
||||
if (ses->fd_.load() == 0) {
|
||||
ESP_LOGD(TAG, "Removing dead event source session");
|
||||
it = this->sessions_.erase(it);
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
|
||||
this->sessions_[i] = this->sessions_.back();
|
||||
this->sessions_.pop_back();
|
||||
} else {
|
||||
ses->loop();
|
||||
++it;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -315,7 +314,10 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
|
||||
protected:
|
||||
std::string url_;
|
||||
std::set<AsyncEventSourceResponse *> sessions_;
|
||||
// Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards).
|
||||
// Linear search is faster than red-black tree overhead for this small dataset.
|
||||
// Only operations needed: add session, remove session, iterate sessions - no need for sorted order.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
connect_handler_t on_connect_{};
|
||||
esphome::web_server::WebServer *web_server_;
|
||||
};
|
||||
|
@@ -447,6 +447,8 @@ async def to_code(config):
|
||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||
)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
|
||||
async def wifi_connected_to_code(config, condition_id, template_arg, args):
|
||||
@@ -468,6 +470,28 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
|
||||
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
|
||||
|
||||
|
||||
def request_wifi_scan_results():
|
||||
"""Request that WiFi scan results be kept in memory after connection.
|
||||
|
||||
Components that need access to scan results after WiFi is connected should
|
||||
call this function during their code generation. This prevents the WiFi component from
|
||||
freeing scan result memory after successful connection.
|
||||
"""
|
||||
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def final_step():
|
||||
"""Final code generation step to configure scan result retention."""
|
||||
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
|
||||
cg.add(
|
||||
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"wifi.configure",
|
||||
WiFiConfigureAction,
|
||||
|
@@ -549,7 +549,7 @@ void WiFiComponent::start_scanning() {
|
||||
// Using insertion sort instead of std::stable_sort saves flash memory
|
||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
||||
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
|
||||
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
||||
const size_t size = results.size();
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
// Make a copy to avoid issues with move semantics during comparison
|
||||
@@ -713,6 +713,12 @@ void WiFiComponent::check_connecting_finished() {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
||||
this->num_retried_ = 0;
|
||||
|
||||
// Free scan results memory unless a component needs them
|
||||
if (!this->keep_scan_results_) {
|
||||
this->scan_result_.clear();
|
||||
this->scan_result_.shrink_to_fit();
|
||||
}
|
||||
|
||||
if (this->fast_connect_) {
|
||||
this->save_fast_connect_settings_();
|
||||
}
|
||||
|
@@ -121,6 +121,14 @@ struct EAPAuth {
|
||||
|
||||
using bssid_t = std::array<uint8_t, 6>;
|
||||
|
||||
// Use std::vector for RP2040 since scan count is unknown (callback-based)
|
||||
// Use FixedVector for other platforms where count is queried first
|
||||
#ifdef USE_RP2040
|
||||
template<typename T> using wifi_scan_vector_t = std::vector<T>;
|
||||
#else
|
||||
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
#endif
|
||||
|
||||
class WiFiAP {
|
||||
public:
|
||||
void set_ssid(const std::string &ssid);
|
||||
@@ -278,7 +286,7 @@ class WiFiComponent : public Component {
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
|
||||
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
||||
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
||||
|
||||
network::IPAddress wifi_soft_ap_ip();
|
||||
|
||||
@@ -316,6 +324,7 @@ class WiFiComponent : public Component {
|
||||
int8_t wifi_rssi();
|
||||
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
|
||||
|
||||
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
|
||||
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
|
||||
@@ -385,7 +394,7 @@ class WiFiComponent : public Component {
|
||||
std::string use_address_;
|
||||
std::vector<WiFiAP> sta_;
|
||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||
std::vector<WiFiScanResult> scan_result_;
|
||||
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||
WiFiAP selected_ap_;
|
||||
WiFiAP ap_;
|
||||
optional<float> output_power_;
|
||||
@@ -424,6 +433,7 @@ class WiFiComponent : public Component {
|
||||
#endif
|
||||
bool enable_on_boot_;
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
|
||||
// Pointers at the end (naturally aligned)
|
||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||
|
@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Count the number of results first
|
||||
auto *head = reinterpret_cast<bss_info *>(arg);
|
||||
size_t count = 0;
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
count++;
|
||||
}
|
||||
|
||||
this->scan_result_.init(count);
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
|
||||
|
@@ -784,7 +784,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
records.resize(number);
|
||||
|
||||
scan_result_.reserve(number);
|
||||
scan_result_.init(number);
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto &record = records[i];
|
||||
bssid_t bssid;
|
||||
|
@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
if (num < 0)
|
||||
return;
|
||||
|
||||
this->scan_result_.reserve(static_cast<unsigned int>(num));
|
||||
this->scan_result_.init(static_cast<unsigned int>(num));
|
||||
for (int i = 0; i < num; i++) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.components import text_sensor, wifi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BSSID,
|
||||
@@ -77,7 +77,9 @@ async def to_code(config):
|
||||
await setup_conf(config, CONF_SSID)
|
||||
await setup_conf(config, CONF_BSSID)
|
||||
await setup_conf(config, CONF_MAC_ADDRESS)
|
||||
await setup_conf(config, CONF_SCAN_RESULTS)
|
||||
if CONF_SCAN_RESULTS in config:
|
||||
await setup_conf(config, CONF_SCAN_RESULTS)
|
||||
wifi.request_wifi_scan_results()
|
||||
await setup_conf(config, CONF_DNS_ADDRESS)
|
||||
if conf := config.get(CONF_IP_ADDRESS):
|
||||
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||
|
@@ -7,6 +7,7 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -14,7 +15,7 @@ namespace esphome {
|
||||
|
||||
template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit AndCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
||||
explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||
bool check(Ts... x) override {
|
||||
for (auto *condition : this->conditions_) {
|
||||
if (!condition->check(x...))
|
||||
@@ -25,12 +26,12 @@ template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<Condition<Ts...> *> conditions_;
|
||||
FixedVector<Condition<Ts...> *> conditions_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit OrCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
||||
explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||
bool check(Ts... x) override {
|
||||
for (auto *condition : this->conditions_) {
|
||||
if (condition->check(x...))
|
||||
@@ -41,7 +42,7 @@ template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<Condition<Ts...> *> conditions_;
|
||||
FixedVector<Condition<Ts...> *> conditions_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
||||
@@ -55,7 +56,7 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
||||
|
||||
template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit XorCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
||||
explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||
bool check(Ts... x) override {
|
||||
size_t result = 0;
|
||||
for (auto *condition : this->conditions_) {
|
||||
@@ -66,7 +67,7 @@ template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<Condition<Ts...> *> conditions_;
|
||||
FixedVector<Condition<Ts...> *> conditions_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
|
||||
|
@@ -197,6 +197,18 @@ template<typename T> class FixedVector {
|
||||
public:
|
||||
FixedVector() = default;
|
||||
|
||||
/// Constructor from initializer list - allocates exact size needed
|
||||
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||
FixedVector(std::initializer_list<T> init_list) {
|
||||
init(init_list.size());
|
||||
size_t idx = 0;
|
||||
for (const auto &item : init_list) {
|
||||
new (data_ + idx) T(item);
|
||||
++idx;
|
||||
}
|
||||
size_ = init_list.size();
|
||||
}
|
||||
|
||||
~FixedVector() { cleanup_(); }
|
||||
|
||||
// Disable copy operations (avoid accidental expensive copies)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
[build]
|
||||
command = "script/build-api-docs"
|
||||
publish = "api-docs"
|
||||
environment = { PYTHON_VERSION = "3.13" }
|
||||
|
@@ -12,13 +12,13 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==41.16.1
|
||||
aioesphomeapi==41.17.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.15 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.12 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.14 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==10.4.0
|
||||
pillow==11.3.0
|
||||
cairosvg==2.8.2
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
|
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO13
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO13
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO13
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
Reference in New Issue
Block a user