1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-10 20:05:48 +00:00

Compare commits

..

22 Commits

Author SHA1 Message Date
J. Nick Koston
d648b3f462 propsals 2025-11-07 22:19:51 -06:00
J. Nick Koston
05d7410afa propsals 2025-11-07 22:05:29 -06:00
J. Nick Koston
32797534a7 propsals 2025-11-07 22:04:58 -06:00
J. Nick Koston
b264c6caac cleanup defines 2025-11-07 18:16:22 -06:00
J. Nick Koston
e3fb074a60 preen 2025-11-07 17:14:50 -06:00
J. Nick Koston
6e7f66d393 missing registry 2025-11-07 16:40:36 -06:00
J. Nick Koston
ac85949f17 cleanups 2025-11-07 16:38:32 -06:00
J. Nick Koston
0962024d99 cleanups 2025-11-07 16:35:24 -06:00
J. Nick Koston
327543303c cleanups 2025-11-07 16:34:37 -06:00
J. Nick Koston
8229e3a471 cleanups 2025-11-07 16:33:01 -06:00
J. Nick Koston
1b6471f4b0 cleanups 2025-11-07 16:30:38 -06:00
J. Nick Koston
c87d07ba70 fixes 2025-11-07 16:15:07 -06:00
J. Nick Koston
fc8dc33023 fixes 2025-11-07 16:13:59 -06:00
J. Nick Koston
c0e4f415f1 Revert "no ifdefs needed on forward decs"
This reverts commit 871c5ddb4e.
2025-11-07 16:10:56 -06:00
J. Nick Koston
871c5ddb4e no ifdefs needed on forward decs 2025-11-07 16:07:54 -06:00
J. Nick Koston
6ef2763cab controller registry 2025-11-07 16:01:45 -06:00
J. Nick Koston
929279dc23 controller registry 2025-11-07 15:55:22 -06:00
J. Nick Koston
6fa0f1e290 controller registry 2025-11-07 15:51:13 -06:00
J. Nick Koston
51eb8ea1d0 controller registry 2025-11-07 15:48:02 -06:00
J. Nick Koston
cbdd663fbf Merge remote-tracking branch 'upstream/dev' into controller_registry 2025-11-07 15:46:57 -06:00
J. Nick Koston
f1009a7468 tweak 2025-11-07 15:44:17 -06:00
J. Nick Koston
295fe8da04 controller registry phase1/2 2025-11-07 15:32:46 -06:00
69 changed files with 2876 additions and 1504 deletions

View File

@@ -206,7 +206,6 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter

View File

@@ -0,0 +1,309 @@
# Sensor Callback Optimization - Zero-Cost Implementation
## The Perfect Optimization
By storing the partition count **in the Sensor class** alongside existing small fields, we achieve a **zero-cost optimization** with only wins and no losses!
## Implementation Design
### Key Insight: Reuse Available Padding
Sensor already has grouped small fields with 1 byte of available space:
```cpp
class Sensor {
protected:
// Existing small members grouped together
int8_t accuracy_decimals_{-1}; // 1 byte
StateClass state_class_{STATE_CLASS_NONE}; // 1 byte (uint8_t enum)
struct SensorFlags {
uint8_t has_accuracy_override : 1;
uint8_t has_state_class_override : 1;
uint8_t force_update : 1;
uint8_t reserved : 5;
} sensor_flags_{}; // 1 byte
uint8_t filtered_count_{0}; // 1 byte ← NEW! Perfect fit!
// Total: 4 bytes (naturally aligned, no padding waste)
};
```
### Callbacks Structure (Heap-Allocated)
```cpp
class Sensor {
protected:
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_;
// Partition layout: [filtered_0, ..., filtered_n-1, raw_0, ..., raw_m-1]
// ^ ^
// 0 filtered_count_
};
```
### Core Methods
```cpp
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
}
// Add to filtered section: append + swap into position
this->callbacks_->push_back(std::move(callback));
if (this->filtered_count_ < this->callbacks_->size() - 1) {
std::swap((*this->callbacks_)[this->filtered_count_],
(*this->callbacks_)[this->callbacks_->size() - 1]);
}
this->filtered_count_++;
}
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
}
// Add to raw section: just append (already at end)
this->callbacks_->push_back(std::move(callback));
}
void Sensor::publish_state(float state) {
this->raw_state = state;
// Call raw callbacks (before filters)
if (this->callbacks_) {
for (size_t i = this->filtered_count_; i < this->callbacks_->size(); i++) {
(*this->callbacks_)[i](state);
}
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
// ... apply filters ...
}
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
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)
if (this->callbacks_) {
for (size_t i = 0; i < this->filtered_count_; i++) {
(*this->callbacks_)[i](state);
}
}
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);
#endif
}
```
## Memory Analysis (ESP32 32-bit)
### Current Implementation
```cpp
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
CallbackManager<void(float)> callback_; // 12 bytes
```
### Partitioned Implementation
```cpp
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_; // 4 bytes
uint8_t filtered_count_{0}; // 0 bytes (uses existing padding slot)
```
## Memory Comparison
| Scenario | Current | Partitioned | Savings |
|----------|---------|-------------|---------|
| **No callbacks** | 16 bytes | 4 bytes | **+12 bytes** ✅ |
| **1 filtered (MQTT)** | 32 bytes | 32 bytes | **±0 bytes** ✅ |
| **1 raw only** | 44 bytes | 32 bytes | **+12 bytes** ✅ |
| **1 raw + 1 filtered** | 60 bytes | 48 bytes | **+12 bytes** ✅ |
| **2 filtered** | 48 bytes | 48 bytes | **±0 bytes** ✅ |
### Detailed Breakdown
**No callbacks:**
- Current: 4 (raw ptr) + 12 (callback_ vec) = 16 bytes
- Partitioned: 4 (callbacks_ ptr) + 0 (count uses existing padding) = **4 bytes**
- **Saves: 12 bytes** ✅
**1 filtered callback (MQTT):**
- Current: 4 + 12 + 16 (function) = 32 bytes
- Partitioned: 4 (ptr) + 12 (vector on heap) + 16 (function) = **32 bytes**
- **Saves: 0 bytes** (ZERO COST!) ✅
**1 raw + 1 filtered:**
- Current: 4 + 12 + 12 (raw vec on heap) + 16 + 16 = 60 bytes
- Partitioned: 4 + 12 + 16 + 16 = **48 bytes**
- **Saves: 12 bytes** ✅
## Real-World Impact
### Typical IoT Device (15 sensors)
**API-only (no MQTT, no automations):**
- Current: 15 × 16 = 240 bytes
- Optimized: 15 × 4 = 60 bytes
- **Saves: 180 bytes** ✅
**With MQTT on all sensors:**
- Current: 15 × 32 = 480 bytes
- Optimized: 15 × 32 = 480 bytes
- **Saves: 0 bytes** (ZERO COST!) ✅
**Mixed (10 API-only + 5 MQTT):**
- Current: (10 × 16) + (5 × 32) = 320 bytes
- Optimized: (10 × 4) + (5 × 32) = 200 bytes
- **Saves: 120 bytes** ✅
### Large Dashboard (50 sensors)
**API-only:**
- Current: 50 × 16 = 800 bytes
- Optimized: 50 × 4 = 200 bytes
- **Saves: 600 bytes** ✅
**With MQTT on 20 sensors:**
- Current: (30 × 16) + (20 × 32) = 1,120 bytes
- Optimized: (30 × 4) + (20 × 32) = 760 bytes
- **Saves: 360 bytes** ✅
## Performance Characteristics
### Time Complexity
- `add_on_state_callback()`: **O(1)** - append + swap
- `add_on_raw_state_callback()`: **O(1)** - append
- `publish_state()` (call raw): **O(m)** - iterate raw section
- `internal_send_state_to_frontend()` (call filtered): **O(n)** - iterate filtered section
### Hot Path Performance
**Before:**
```cpp
if (this->raw_callback_) {
this->raw_callback_->call(state); // Separate container
}
// ...
this->callback_.call(state); // Separate container
```
**After:**
```cpp
// Call raw callbacks
if (this->callbacks_) {
for (size_t i = filtered_count_; i < callbacks_->size(); i++) {
(*callbacks_)[i](state);
}
}
// ...
// Call filtered callbacks
if (this->callbacks_) {
for (size_t i = 0; i < filtered_count_; i++) {
(*callbacks_)[i](state);
}
}
```
**Performance impact:**
- ✅ Better cache locality (single vector instead of two containers)
- ✅ No branching inside loops (vs checking callback types)
- ✅ Tight loops for typical 0-2 callbacks case
- ⚠️ One extra nullptr check (negligible, likely free with branch prediction)
## Advantages
### Memory
1.**12 bytes saved** per sensor without callbacks (most common after Controller Registry)
2.**ZERO cost** for MQTT-enabled sensors (32 → 32 bytes)
3.**12 bytes saved** for sensors with both raw + filtered callbacks
4.**No padding waste** (reuses existing padding slot in Sensor class)
### Architecture
1.**Cleaner:** ONE vector instead of TWO separate CallbackManager instances
2.**Simpler:** Partitioned vector is more elegant than dual containers
3.**Better cache locality:** Callbacks stored contiguously
4.**O(1) insertion:** Both add operations use append (+ optional swap)
### Code Quality
1.**No new fields in hot path:** filtered_count_ reuses padding
2.**No branching in iteration:** Direct range iteration
3.**Order preservation not needed:** Callbacks are independent
## Implementation Files
### Modified Files
- `esphome/components/sensor/sensor.h`
- `esphome/components/sensor/sensor.cpp`
### Changes Required
1. Replace callback storage with partitioned vector
2. Update `add_on_state_callback()` to use swap-based insertion
3. Update `add_on_raw_state_callback()` to append
4. Update `publish_state()` to iterate raw section
5. Update `internal_send_state_to_frontend()` to iterate filtered section
6. Add `filtered_count_` field (uses existing padding)
## TextSensor Implementation
TextSensor can use the **exact same pattern**:
```cpp
class TextSensor {
protected:
std::unique_ptr<std::vector<std::function<void(std::string)>>> callbacks_;
uint8_t filtered_count_{0}; // Store in class (check for available padding)
};
```
Same benefits apply!
## Migration Risk Assessment
### Low Risk
- ✅ No API changes (public methods unchanged)
- ✅ Callback behavior identical (same execution order within each type)
- ✅ Only internal implementation changes
- ✅ Well-tested pattern (partitioned vectors common in CS)
### Testing Strategy
1. Unit tests: Verify callback execution order preserved
2. Integration tests: Test with MQTT, automations, copy components
3. Memory benchmarks: Confirm actual RAM savings on real devices
4. Regression tests: Ensure no behavior changes for existing configs
## Recommendation
**IMPLEMENT IMMEDIATELY**
This optimization has:
-**Zero cost** for MQTT users (32 → 32 bytes)
-**12-byte savings** for API-only sensors (most common)
-**12-byte savings** for sensors with automations
-**Better architecture** (one container vs two)
-**No downsides** whatsoever
**Expected savings for typical device: 150-600 bytes**
This is a **pure win** optimization with no trade-offs!
## Implementation Priority
### Phase 1: Sensor ⭐⭐⭐ (HIGHEST PRIORITY)
- Most common entity type
- Biggest impact
- Zero cost even for MQTT users
- **Start here!**
### Phase 2: TextSensor ⭐⭐
- Second most common entity with raw callbacks
- Same pattern as Sensor
### Phase 3: Other entities (simple lazy vector) ⭐
- BinarySensor, Switch, etc. don't have raw callbacks
- Can use simpler lazy-allocated vector
- Still save 12 bytes when no callbacks

View File

@@ -0,0 +1,845 @@
# CallbackManager Optimization Plan
**Note:** ESPHome uses C++20 (gnu++20), so implementations leverage modern C++ features:
- **Concepts** for type constraints and better error messages
- **Designated initializers** for cleaner struct initialization
- **consteval** for compile-time validation
- **Requires clauses** for inline constraints
## Current State
### Memory Profile (ESP32 - 32-bit)
```cpp
sizeof(std::function<void(T)>): 32 bytes
sizeof(void*): 4 bytes
sizeof(function pointer): 4 bytes
```
### Current Implementation
```cpp
template<typename... Ts> class CallbackManager<void(Ts...)> {
public:
void add(std::function<void(Ts...)> &&callback) {
this->callbacks_.push_back(std::move(callback));
}
void call(Ts... args) {
for (auto &cb : this->callbacks_)
cb(args...);
}
size_t size() const { return this->callbacks_.size(); }
protected:
std::vector<std::function<void(Ts...)>> callbacks_;
};
```
### Memory Cost Per Instance
- **Per callback:** 32 bytes (std::function storage)
- **Vector reallocation code:** ~132 bytes (`_M_realloc_append` template instantiation)
- **Example (1 callback):** 32 + 132 = 164 bytes
### Codebase Usage
- **Total CallbackManager instances:** ~67 files
- **Estimated total callbacks:** 100-150 across all components
- **Examples:**
- `sensor.h`: `CallbackManager<void(float)>` - multiple callbacks per sensor
- `esp32_ble_tracker.h`: `CallbackManager<void(ScannerState)>` - 1 callback (bluetooth_proxy)
- `esp32_improv.h`: `CallbackManager<void(State, Error)>` - up to 5 callbacks (automation triggers)
- `climate.h`: `CallbackManager<void()>` - multiple callbacks for state/control
### Current Usage Pattern
All callbacks currently use lambda captures:
```cpp
// bluetooth_proxy.cpp
parent_->add_scanner_state_callback([this](ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
// sensor.cpp (via automation)
sensor->add_on_state_callback([this](float state) {
this->trigger(state);
});
```
---
## Optimization Options
### Option 1: Function Pointer + Context (Recommended)
**C++20 Implementation (Type-Safe with Concepts):**
```cpp
#include <concepts>
#include <type_traits>
// Concept to validate callback signature
template<typename F, typename Context, typename... Ts>
concept CallbackFunction = requires(F func, Context* ctx, Ts... args) {
{ func(ctx, args...) } -> std::same_as<void>;
};
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes - type-erased invoker
void* context; // 4 bytes - captured context
// Total: 8 bytes
};
// Type-safe invoker template - knows real context type
template<typename Context>
static void invoke(void* ctx, Ts... args) {
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(
*static_cast<void**>(ctx)
);
auto typed_ctx = static_cast<Context*>(
*reinterpret_cast<void**>(static_cast<char*>(ctx) + sizeof(void*))
);
typed_func(typed_ctx, args...);
}
std::vector<Callback> callbacks_;
public:
// Type-safe registration with concept constraint
template<typename Context>
requires CallbackFunction<void(*)(Context*, Ts...), Context, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
// Use designated initializers (C++20)
callbacks_.push_back({
.invoker = [](void* storage, Ts... args) {
// Extract function pointer and context from packed storage
void* func_and_ctx[2];
std::memcpy(func_and_ctx, storage, sizeof(func_and_ctx));
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func_and_ctx[0]);
auto typed_ctx = static_cast<Context*>(func_and_ctx[1]);
typed_func(typed_ctx, args...);
},
.context = nullptr // Will store packed data
});
// Pack function pointer and context into the callback storage
void* func_and_ctx[2] = { reinterpret_cast<void*>(func), context };
std::memcpy(&callbacks_.back(), func_and_ctx, sizeof(func_and_ctx));
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(&cb, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
};
```
**Cleaner C++20 Implementation (12 bytes, simpler):**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, void*, Ts...); // 4 bytes - generic invoker
void* func_ptr; // 4 bytes - actual function
void* context; // 4 bytes - context
// Total: 12 bytes (still 20 bytes saved vs std::function!)
};
template<typename Context>
static consteval auto make_invoker() {
return +[](void* func, void* ctx, Ts... args) {
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func);
typed_func(static_cast<Context*>(ctx), args...);
};
}
std::vector<Callback> callbacks_;
public:
// C++20 concepts for type safety
template<typename Context>
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
// C++20 designated initializers
callbacks_.push_back({
.invoker = make_invoker<Context>(),
.func_ptr = reinterpret_cast<void*>(func),
.context = context
});
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.func_ptr, cb.context, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
constexpr bool empty() const { return callbacks_.empty(); }
};
```
**Most Efficient C++20 Implementation (8 bytes):**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes
void* context; // 4 bytes
// Total: 8 bytes - maximum savings!
};
// C++20: consteval ensures compile-time evaluation
template<typename Context>
static consteval auto make_invoker() {
// The + forces decay to function pointer
return +[](void* ctx, Ts... args) {
// Unpack the storage struct
struct Storage {
void (*func)(Context*, Ts...);
Context* context;
};
auto* storage = static_cast<Storage*>(ctx);
storage->func(storage->context, args...);
};
}
std::vector<Callback> callbacks_;
public:
template<typename Context>
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
// Allocate storage for function + context
struct Storage {
void (*func)(Context*, Ts...);
Context* context;
};
auto* storage = new Storage{func, context};
callbacks_.push_back({
.invoker = make_invoker<Context>(),
.context = storage
});
}
~CallbackManager() {
// Clean up storage
for (auto& cb : callbacks_) {
delete static_cast<void*>(cb.context);
}
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.context, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
};
```
**Simplest C++20 Implementation (Recommended):**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, void*, Ts...); // 4 bytes
void* func_ptr; // 4 bytes
void* context; // 4 bytes
// Total: 12 bytes
};
template<typename Context>
static void invoke(void* func, void* ctx, Ts... args) {
reinterpret_cast<void(*)(Context*, Ts...)>(func)(static_cast<Context*>(ctx), args...);
}
std::vector<Callback> callbacks_;
public:
template<typename Context>
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
callbacks_.push_back({
.invoker = &invoke<Context>,
.func_ptr = reinterpret_cast<void*>(func),
.context = context
});
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.func_ptr, cb.context, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
};
```
**C++20 Benefits:**
-**Concepts** provide clear compile errors
-**Designated initializers** make code more readable
-**consteval** ensures compile-time evaluation
-**constexpr** improvements allow more compile-time validation
-**Requires clauses** document constraints inline
**Usage Changes:**
```cpp
// OLD (lambda):
parent_->add_scanner_state_callback([this](ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
// NEW (static function + context):
static void scanner_state_callback(BluetoothProxy* proxy, ScannerState state) {
if (proxy->api_connection_ != nullptr) {
proxy->send_bluetooth_scanner_state_(state);
}
}
// Registration
parent_->add_scanner_state_callback(scanner_state_callback, this);
```
**Savings:**
- **Per callback:** 24 bytes (32 → 8) or 20 bytes (32 → 12 for simpler version)
- **RAM saved (100-150 callbacks):** 2.4 - 3.6 KB
- **Flash saved:** ~5-10 KB (eliminates std::function template instantiations)
**Pros:**
- ✅ Maximum memory savings (75% reduction)
- ✅ Type-safe at registration time
- ✅ No virtual function overhead
- ✅ Works with all capture patterns
- ✅ Simple implementation
**Cons:**
- ❌ Requires converting lambdas to static functions
- ❌ Changes API for all 67 CallbackManager users
- ❌ More verbose at call site
---
### Option 2: Member Function Pointers
**Implementation:**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes
void* obj; // 4 bytes
// Total: 8 bytes
};
template<typename T, void (T::*Method)(Ts...)>
static void invoke_member(void* obj, Ts... args) {
(static_cast<T*>(obj)->*Method)(args...);
}
std::vector<Callback> callbacks_;
public:
// Register a member function
template<typename T, void (T::*Method)(Ts...)>
void add(T* obj) {
callbacks_.push_back({
&invoke_member<T, Method>,
obj
});
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.obj, args...);
}
}
size_t size() const { return callbacks_.size(); }
};
```
**Usage Changes:**
```cpp
// Add a method to BluetoothProxy
void BluetoothProxy::on_scanner_state_changed(ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
}
// Register it
parent_->add_scanner_state_callback<BluetoothProxy,
&BluetoothProxy::on_scanner_state_changed>(this);
```
**Savings:**
- **Per callback:** 24 bytes (32 → 8)
- **RAM saved:** 2.4 - 3.6 KB
- **Flash saved:** ~5-10 KB
**Pros:**
- ✅ Same memory savings as Option 1
- ✅ Most type-safe (member function pointers)
- ✅ No static functions needed
- ✅ Clean separation of callback logic
**Cons:**
- ❌ Verbose syntax at registration: `add<Type, &Type::method>(this)`
- ❌ Requires adding methods to classes
- ❌ Can't capture additional state beyond `this`
- ❌ Template parameters at call site are ugly
---
### Option 3: Hybrid (Backward Compatible)
**Implementation:**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes
void* data; // 4 bytes
bool is_std_function; // 1 byte + 3 padding = 4 bytes
// Total: 12 bytes
};
std::vector<Callback> callbacks_;
public:
// Optimized: function pointer + context
template<typename Context>
void add(void (*func)(Context*, Ts...), Context* context) {
callbacks_.push_back({
[](void* ctx, Ts... args) {
auto cb = static_cast<Callback*>(ctx);
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(cb->data);
auto typed_ctx = static_cast<Context*>(*reinterpret_cast<void**>(
static_cast<char*>(cb) + offsetof(Callback, data)
));
typed_func(typed_ctx, args...);
},
reinterpret_cast<void*>(func),
false
});
}
// Legacy: std::function support (for gradual migration)
void add(std::function<void(Ts...)>&& func) {
auto* stored = new std::function<void(Ts...)>(std::move(func));
callbacks_.push_back({
[](void* ctx, Ts... args) {
(*static_cast<std::function<void(Ts...)>*>(ctx))(args...);
},
stored,
true
});
}
~CallbackManager() {
for (auto& cb : callbacks_) {
if (cb.is_std_function) {
delete static_cast<std::function<void(Ts...)>*>(cb.data);
}
}
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(&cb, args...);
}
}
size_t size() const { return callbacks_.size(); }
};
```
**Usage:**
```cpp
// NEW (optimized):
parent_->add_scanner_state_callback(scanner_state_callback, this);
// OLD (still works - gradual migration):
parent_->add_scanner_state_callback([this](ScannerState state) {
// ... lambda still works
});
```
**Savings:**
- **Per optimized callback:** 20 bytes (32 → 12)
- **Per legacy callback:** 0 bytes (still uses std::function)
- **Allows gradual migration**
**Pros:**
- ✅ Backward compatible
- ✅ Gradual migration path
- ✅ Mix optimized and legacy in same codebase
- ✅ No breaking changes
**Cons:**
- ❌ More complex implementation
- ❌ Need to track which callbacks need cleanup
- ❌ Extra bool field (padding makes it 12 bytes instead of 8)
- ❌ std::function still compiled in
---
### Option 4: FixedVector (Keep std::function, Optimize Vector)
**Implementation:**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
public:
void add(std::function<void(Ts...)> &&callback) {
if (this->callbacks_.empty()) {
// Most CallbackManagers have 1-5 callbacks
this->callbacks_.init(5);
}
this->callbacks_.push_back(std::move(callback));
}
void call(Ts... args) {
for (auto &cb : this->callbacks_)
cb(args...);
}
size_t size() const { return this->callbacks_.size(); }
protected:
FixedVector<std::function<void(Ts...)>> callbacks_; // Changed from std::vector
};
```
**Savings:**
- **Per callback:** 0 bytes (still 32 bytes)
- **Per instance:** ~132 bytes (eliminates `_M_realloc_append`)
- **Flash saved:** ~5-10 KB (one less vector template instantiation per type)
- **Total:** ~132 bytes × ~20 unique callback types = ~2.6 KB
**Pros:**
- ✅ No API changes
- ✅ Drop-in replacement
- ✅ Eliminates vector reallocation machinery
- ✅ Zero migration cost
**Cons:**
- ❌ No per-callback savings
- ❌ std::function still 32 bytes each
- ❌ Must guess max size at runtime
- ❌ Can still overflow if guess is wrong
---
### Option 5: Template Parameter for Storage (Advanced)
**Implementation:**
```cpp
enum class CallbackStorage {
FUNCTION, // Use std::function (default, most flexible)
FUNCTION_PTR // Use function pointer + context (optimal)
};
template<typename... Ts, CallbackStorage Storage = CallbackStorage::FUNCTION>
class CallbackManager<void(Ts...)> {
// Specialize implementation based on Storage parameter
};
// Default: std::function (backward compatible)
template<typename... Ts>
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION> {
protected:
std::vector<std::function<void(Ts...)>> callbacks_;
// ... current implementation
};
// Optimized: function pointer + context
template<typename... Ts>
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION_PTR> {
private:
struct Callback {
void (*func)(void*, Ts...);
void* context;
};
std::vector<Callback> callbacks_;
// ... Option 1 implementation
};
```
**Usage:**
```cpp
// Old components (no changes):
CallbackManager<void(float)> callback_; // Uses std::function by default
// Optimized components:
CallbackManager<void(ScannerState), CallbackStorage::FUNCTION_PTR> scanner_state_callbacks_;
```
**Savings:**
- **Opt-in per component**
- **Same as Option 1 for optimized components**
**Pros:**
- ✅ Gradual migration
- ✅ No breaking changes
- ✅ Explicit opt-in per component
- ✅ Clear which components are optimized
**Cons:**
- ❌ Complex template metaprogramming
- ❌ Two implementations to maintain
- ❌ Template parameter pollution
- ❌ Harder to understand codebase
---
## Comparison Matrix
| Option | Per-Callback Savings | Flash Savings | API Changes | Complexity | Migration Cost |
|--------|---------------------|---------------|-------------|------------|----------------|
| **1. Function Ptr + Context** | **24 bytes** (75%) | **~10 KB** | Yes | Low | High (67 files) |
| **2. Member Function Ptrs** | **24 bytes** (75%) | **~10 KB** | Yes | Medium | High + class changes |
| **3. Hybrid** | **20 bytes** (opt-in) | **~8 KB** | No | High | Low (gradual) |
| **4. FixedVector** | **0 bytes** | **~3 KB** | No | Low | None |
| **5. Template Parameter** | **24 bytes** (opt-in) | **~10 KB** | Optional | High | Medium |
---
## Migration Effort Estimate
### Option 1 (Function Pointer + Context)
**Files to change:** ~67 files with CallbackManager usage
**Per-file changes:**
1. Convert lambda to static function (5 min)
2. Update registration call (1 min)
3. Test (5 min)
**Estimate:** ~11 min × 67 files = **~12 hours** (assuming some files have multiple callbacks)
**High-impact components to prioritize:**
- `sensor.h` / `sensor.cpp` - many sensor callbacks
- `esp32_ble_tracker.h` - BLE callbacks
- `climate.h` - climate callbacks
- `binary_sensor.h` - binary sensor callbacks
### Option 4 (FixedVector)
**Files to change:** 1 file (`esphome/core/helpers.h`)
**Changes:**
1. Change `std::vector` to `FixedVector` in CallbackManager
2. Initialize with reasonable default size (e.g., 5)
3. Test across codebase
**Estimate:** **~1 hour**
---
## Recommendations
### Immediate Action: Option 4 (FixedVector)
**Why:**
- Zero migration cost
- Immediate ~3 KB flash savings
- No API changes
- Low risk
**Implementation:**
```cpp
template<typename... Ts> class CallbackManager<void(Ts...)> {
public:
void add(std::function<void(Ts...)> &&callback) {
if (this->callbacks_.empty()) {
this->callbacks_.init(8); // Most have < 8 callbacks
}
this->callbacks_.push_back(std::move(callback));
}
// ... rest unchanged
protected:
FixedVector<std::function<void(Ts...)>> callbacks_;
};
```
### Long-term: Option 1 (Function Pointer + Context)
**Why:**
- Maximum savings (2.4-3.6 KB RAM + 10 KB flash)
- Clean, simple implementation
- Type-safe
- Well-tested pattern
**Migration Strategy:**
1. Implement new `CallbackManager` in `helpers.h`
2. Migrate high-impact components first:
- Core components (sensor, binary_sensor, climate)
- BLE components (esp32_ble_tracker, bluetooth_proxy)
- Network components (api, mqtt)
3. Create helper macros to reduce boilerplate
4. Migrate remaining components over 2-3 releases
**Helper Macro Example:**
```cpp
// Define a callback wrapper
#define CALLBACK_WRAPPER(Class, Method, ...) \
static void Method##_callback(Class* self, ##__VA_ARGS__) { \
self->Method(__VA_ARGS__); \
}
// In class:
class BluetoothProxy {
CALLBACK_WRAPPER(BluetoothProxy, on_scanner_state, ScannerState state)
void on_scanner_state(ScannerState state) {
// Implementation
}
void setup() {
parent_->add_scanner_state_callback(on_scanner_state_callback, this);
}
};
```
---
## Testing Plan
### Phase 1: Unit Tests
- Test CallbackManager with various signatures
- Test multiple callbacks (1, 5, 10, 50)
- Test callback removal/cancellation
- Test edge cases (empty, nullptr, etc.)
### Phase 2: Integration Tests
- Create test YAML with heavily-used callbacks
- Run on ESP32, ESP8266, RP2040
- Measure before/after memory usage
- Verify no functional regressions
### Phase 3: Component Tests
- Test high-impact components:
- sensor with multiple state callbacks
- esp32_improv with all automation triggers
- climate with state/control callbacks
- Measure memory with `esphome analyze-memory`
---
## Risk Analysis
### Option 1 Risks
**Risk: Breaking change across 67 files**
- **Mitigation:** Gradual rollout over 2-3 releases
- **Mitigation:** Extensive testing on real hardware
**Risk: Static function verbosity**
- **Mitigation:** Helper macros (see above)
- **Mitigation:** Code generation from Python
**Risk: Missing captures**
- **Mitigation:** Static analysis to find lambda captures
- **Mitigation:** Compile-time errors for incorrect usage
### Option 4 Risks
**Risk: Buffer overflow if size guess is wrong**
- **Mitigation:** Choose conservative default (8)
- **Mitigation:** Add runtime warning on resize
- **Mitigation:** Monitor in CI/testing
**Risk: Still uses std::function (32 bytes each)**
- **Mitigation:** Follow up with Option 1 migration
- **Mitigation:** This is a stepping stone, not final solution
---
## Implementation Timeline
### Week 1: Option 4 (Quick Win)
- Implement FixedVector in CallbackManager
- Test across codebase
- Create PR with memory analysis
- **Expected savings:** ~3 KB flash
### Month 1-2: Option 1 (Core Components)
- Implement function pointer CallbackManager
- Migrate sensor, binary_sensor, climate
- Create helper macros
- **Expected savings:** ~1 KB RAM + 5 KB flash
### Month 3-4: Option 1 (Remaining Components)
- Migrate BLE components
- Migrate network components (api, mqtt)
- Migrate automation components
- **Expected savings:** ~2 KB RAM + 10 KB flash total
### Month 5: Cleanup
- Remove std::function CallbackManager
- Update documentation
- Blog post about optimization
---
## Conclusion
**Recommended Approach:**
1. **Immediate (Week 1):** Implement Option 4 (FixedVector)
- Low risk, zero migration cost
- ~3 KB flash savings
- Sets foundation for Option 1
2. **Short-term (Month 1-2):** Begin Option 1 migration
- Start with high-impact components
- ~1-2 KB RAM + 5 KB flash savings
- Validate approach
3. **Long-term (Month 3-6):** Complete Option 1 migration
- Migrate all components
- ~3-4 KB total RAM + 10 KB flash savings
- Remove std::function variant
**Total Expected Savings:**
- **RAM:** 2.4 - 3.6 KB (75% reduction per callback)
- **Flash:** 8 - 13 KB (vector overhead + template instantiations)
- **Performance:** Slightly faster (no std::function indirection)
This is significant for ESP8266 (80 KB RAM, 1 MB flash) and beneficial for all platforms.

View File

@@ -0,0 +1,75 @@
# Callback Optimization Analysis - Why It Failed
## Goal
Convert stateful lambdas in CallbackManager to stateless function pointers to reduce flash usage.
## Approach Tested
### Attempt 1: Discriminated Union in CallbackManager
**Changed:** `CallbackManager` to use union with discriminator (like `TemplatableValue`)
- Stateless lambdas → function pointer (8 bytes)
- Stateful lambdas → heap-allocated `std::function*` (8 bytes struct + 32 bytes heap)
**Result:**
-**+300 bytes heap usage** (37-38 callbacks × 8 bytes overhead)
- ✅ Flash savings potential: ~200-400 bytes per stateless callback
- **Verdict:** RAM is more precious than flash on ESP8266 - rejected
### Attempt 2: Convert Individual Callbacks to Stateless
**Changed:** API logger callback from `[this]` lambda to static member function
- Used existing `global_api_server` pointer
- Made callback stateless (convertible to function pointer)
**Result:**
```
Removed:
- Lambda _M_invoke: 103 bytes
- Lambda _M_manager: 20 bytes
Added:
- log_callback function: 104 bytes
- Function pointer _M_invoke: 20 bytes
- Function pointer _M_manager: 20 bytes
- Larger setup(): 7 bytes
Net: +32 bytes flash ❌
```
**Why it failed:**
Even though the callback became stateless, `CallbackManager` still uses `std::vector<std::function<void(Ts...)>>`. The function pointer STILL gets wrapped in `std::function`, generating the same template instantiation overhead. We just moved the code from a lambda to a static function.
## Root Cause
The optimization **requires BOTH**:
1. ✅ Stateless callback (function pointer)
2. ❌ Modified `CallbackManager` to store function pointers directly without `std::function` wrapper
Without modifying `CallbackManager`, converting individual callbacks to function pointers provides **no benefit** and actually **increases** code size slightly due to the extra function definition.
## Conclusion
This optimization path is a **dead end** for ESPHome because:
1. **Discriminated union approach**: Increases heap by 300 bytes (unacceptable for ESP8266)
2. **Individual callback conversion**: Increases flash by 32+ bytes (no benefit without CallbackManager changes)
The current `std::vector<std::function<...>>` approach is already optimal for the use case where most callbacks capture state.
## Alternative Approaches Considered
1. **Create separate `StatelessCallbackManager`**: Would require changing all call sites, not worth the complexity
2. **Template parameter to select storage type**: Same issue - requires modifying many components
3. **Hand-pick specific callbacks**: Provides no benefit as shown in Attempt 2
## Recommendation
**Do not pursue this optimization.** The RAM/flash trade-offs are unfavorable for embedded systems where RAM is typically more constrained than flash.
---
**Test Results:**
- Platform: ESP8266-Arduino
- Component: API
- Result: +32 bytes flash (0.01% increase)
- Status: Reverted
🤖 Analysis by Claude Code

View File

@@ -0,0 +1,256 @@
# Callback Optimization Implementation Plan
## Analysis Summary
After Controller Registry (PR #11772), callback infrastructure can be further optimized:
**Current overhead per entity (ESP32 32-bit):**
- No callbacks: 16 bytes (4-byte ptr + 12-byte empty vector)
- With callbacks: 32+ bytes (16 baseline + 16+ per callback)
**Opportunity:** After Controller Registry, most entities have **zero callbacks** (API/WebServer use registry instead). We can save 12 bytes per entity by lazy allocation.
## Entity Types by Callback Needs
### Entities with ONLY filtered callbacks (most)
- Climate, Fan, Light, Cover
- Switch, Lock, Valve
- Number, Select, Text, Button
- AlarmControlPanel, MediaPlayer
- BinarySensor, Event, Update, DateTime
**Optimization:** Simple lazy-allocated vector
### Entities with raw AND filtered callbacks
- **Sensor** - has raw callbacks for automation triggers
- **TextSensor** - has raw callbacks for automation triggers
**Optimization:** Partitioned vector (filtered | raw)
## Proposed Implementations
### Option 1: Simple Lazy Vector (for entities without raw callbacks)
```cpp
class Climate {
protected:
std::unique_ptr<std::vector<std::function<void(Climate&)>>> state_callback_;
};
void Climate::add_on_state_callback(std::function<void(Climate&)> &&callback) {
if (!this->state_callback_) {
this->state_callback_ = std::make_unique<std::vector<std::function<void(Climate&)>>>();
}
this->state_callback_->push_back(std::move(callback));
}
void Climate::publish_state() {
if (this->state_callback_) {
for (auto &cb : *this->state_callback_) {
cb(*this);
}
}
}
```
**Memory (ESP32):**
- No callbacks: 4 bytes (saves 12 vs current)
- 1 callback: 36 bytes (costs 4 vs current)
- Net: Positive for API-only devices
### Option 2: Partitioned Vector (for Sensor & TextSensor)
```cpp
class Sensor {
protected:
struct Callbacks {
std::vector<std::function<void(float)>> callbacks_;
uint8_t filtered_count_{0}; // Partition point: [filtered | raw]
void add_filtered(std::function<void(float)> &&fn) {
callbacks_.push_back(std::move(fn));
if (filtered_count_ < callbacks_.size() - 1) {
std::swap(callbacks_[filtered_count_], callbacks_[callbacks_.size() - 1]);
}
filtered_count_++;
}
void add_raw(std::function<void(float)> &&fn) {
callbacks_.push_back(std::move(fn)); // Append to raw section
}
void call_filtered(float value) {
for (size_t i = 0; i < filtered_count_; i++) {
callbacks_[i](value);
}
}
void call_raw(float value) {
for (size_t i = filtered_count_; i < callbacks_.size(); i++) {
callbacks_[i](value);
}
}
};
std::unique_ptr<Callbacks> callbacks_;
};
```
**Why partitioned:**
- Maintains separation of raw (pre-filter) vs filtered (post-filter) callbacks
- O(1) insertion via swap (order doesn't matter)
- No branching in hot path
- Saves 12 bytes when no callbacks
## Memory Impact Analysis
### Scenario 1: API-only device (10 sensors, no MQTT, no automations)
**Current:** 10 × 16 = 160 bytes
**Optimized:** 10 × 4 = 40 bytes
**Saves: 120 bytes**
### Scenario 2: MQTT-enabled device (10 sensors with MQTT)
**Current:** 10 × 32 = 320 bytes
**Optimized:** 10 × 36 = 360 bytes
**Costs: 40 bytes** ⚠️
### Scenario 3: Mixed device (5 API-only + 5 MQTT)
**Current:** (5 × 16) + (5 × 32) = 240 bytes
**Optimized:** (5 × 4) + (5 × 36) = 200 bytes
**Saves: 40 bytes**
### Scenario 4: Sensor with automation (1 raw + 1 filtered)
**Current:** 16 + 12 + 16 + 16 = 60 bytes
**Optimized:** 4 + 16 + 32 = 52 bytes
**Saves: 8 bytes**
## Implementation Strategy
### Phase 1: Simple Entities (high impact, low complexity)
1. **Climate** (common, no raw callbacks)
2. **Fan** (common, no raw callbacks)
3. **Cover** (common, no raw callbacks)
4. **Switch** (very common, no raw callbacks)
5. **Lock** (no raw callbacks)
**Change:** Replace `CallbackManager<void(...)> callback_` with `std::unique_ptr<std::vector<std::function<...>>>`
### Phase 2: Sensor & TextSensor (more complex)
1. **Sensor** (most common entity, has raw callbacks)
2. **TextSensor** (common, has raw callbacks)
**Change:** Implement partitioned vector approach
### Phase 3: Remaining Entities
- BinarySensor, Number, Select, Text
- Light, Valve, AlarmControlPanel
- MediaPlayer, Button, Event, Update, DateTime
**Change:** Simple lazy vector
## Code Template for Simple Entities
```cpp
// Header (.h)
class EntityType {
public:
void add_on_state_callback(std::function<void(Args...)> &&callback);
protected:
std::unique_ptr<std::vector<std::function<void(Args...)>>> state_callback_;
};
// Implementation (.cpp)
void EntityType::add_on_state_callback(std::function<void(Args...)> &&callback) {
if (!this->state_callback_) {
this->state_callback_ = std::make_unique<std::vector<std::function<void(Args...)>>>();
}
this->state_callback_->push_back(std::move(callback));
}
void EntityType::publish_state(...) {
// ... state update logic ...
if (this->state_callback_) {
for (auto &cb : *this->state_callback_) {
cb(...);
}
}
#ifdef USE_CONTROLLER_REGISTRY
ControllerRegistry::notify_entity_update(this);
#endif
}
```
## Testing Strategy
1. **Unit tests:** Verify callback ordering/execution unchanged
2. **Integration tests:** Test with MQTT, automations, copy components
3. **Memory benchmarks:** Measure actual flash/RAM impact
4. **Compatibility:** Ensure no API breakage
## Expected Results
**For typical ESPHome devices after Controller Registry:**
- Most entities: API/WebServer only (no callbacks)
- Some entities: MQTT (1 callback)
- Few entities: Automations (1-2 callbacks)
**Memory savings:**
- Device with 20 entities, 5 with MQTT: ~180 bytes saved
- Device with 50 entities, 10 with MQTT: ~480 bytes saved
**Trade-off:**
- Entities without callbacks: Save 12 bytes ✅
- Entities with callbacks: Cost 4 bytes ⚠️
- Net benefit: Positive for most devices
## Risks & Mitigation
**Risk 1:** Increased complexity
- **Mitigation:** Start with simple entities first, template for reuse
**Risk 2:** Performance regression
- **Mitigation:** Minimal - just nullptr check (likely free with branch prediction)
**Risk 3:** Edge cases with callback order
- **Mitigation:** Order already undefined within same callback type
## Open Questions
1. Should we template the Callbacks struct for reuse across entity types?
2. Should Phase 1 include a memory benchmark before expanding?
3. Should we make this configurable (compile-time flag)?
## Files Modified
### Phase 1 (Simple Entities)
- `esphome/components/climate/climate.h`
- `esphome/components/climate/climate.cpp`
- `esphome/components/fan/fan.h`
- `esphome/components/fan/fan.cpp`
- `esphome/components/cover/cover.h`
- `esphome/components/cover/cover.cpp`
- (etc. for switch, lock)
### Phase 2 (Partitioned)
- `esphome/components/sensor/sensor.h`
- `esphome/components/sensor/sensor.cpp`
- `esphome/components/text_sensor/text_sensor.h`
- `esphome/components/text_sensor/text_sensor.cpp`
### Phase 3 (Remaining)
- All other entity types
## Conclusion
**Recommendation: Implement in phases**
1. Start with Climate (common entity, simple change)
2. Measure impact on real device
3. If positive, proceed with other simple entities
4. Implement partitioned approach for Sensor/TextSensor
5. Complete remaining entity types
Expected net savings: **50-500 bytes per typical device**, depending on entity count and MQTT usage.

118
callback_usage_analysis.md Normal file
View File

@@ -0,0 +1,118 @@
# add_on_state_callback Usage Analysis
## Summary
After the Controller Registry migration (PR #11772), `add_on_state_callback` is still widely used in the codebase, but for **legitimate reasons** - components that genuinely need per-entity state tracking.
## Usage Breakdown
### 1. **MQTT Components** (~17 uses)
**Purpose:** Per-entity MQTT configuration requires callbacks
- Each MQTT component instance needs to publish to custom topics with custom QoS/retain settings
- Cannot use Controller pattern due to per-entity configuration overhead
- Examples: `mqtt_sensor.cpp`, `mqtt_climate.cpp`, `mqtt_number.cpp`, etc.
```cpp
this->sensor_->add_on_state_callback([this](float state) {
this->publish_state(state);
});
```
### 2. **Copy Components** (~10 uses)
**Purpose:** Mirror state from one entity to another
- Each copy instance tracks a different source entity
- Legitimate use of callbacks for entity-to-entity synchronization
- Examples: `copy_sensor.cpp`, `copy_fan.cpp`, `copy_select.cpp`, etc.
```cpp
source_->add_on_state_callback([this](const std::string &value) {
this->publish_state(value);
});
```
### 3. **Derivative Sensors** (~5-7 uses)
**Purpose:** Compute derived values from source sensors
- **integration_sensor:** Integrates sensor values over time
- **total_daily_energy:** Tracks cumulative energy
- **combination:** Combines multiple sensor values
- **graph:** Samples sensor data for display
- **duty_time:** Tracks on-time duration
- **ntc/absolute_humidity/resistance:** Mathematical transformations
```cpp
this->sensor_->add_on_state_callback([this](float state) {
this->process_sensor_value_(state);
});
```
### 4. **Climate/Cover with Sensors** (~10-15 uses)
**Purpose:** External sensors providing feedback to control loops
- **feedback_cover:** Binary sensors for open/close/obstacle detection
- **bang_bang/pid/thermostat:** External temperature sensors for climate control
- **climate_ir (toshiba/yashima/heatpumpir):** Temperature sensors for IR climate
```cpp
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
// Trigger control loop update
});
```
### 5. **Entity Base Classes** (~10-15 definitions)
**Purpose:** Provide the callback interface for all entities
- Not actual usage, just the method definitions
- Examples: `sensor.cpp::add_on_state_callback()`, `climate.cpp::add_on_state_callback()`, etc.
### 6. **Automation Trigger Classes** (~15-20 definitions)
**Purpose:** User-defined YAML automations need callbacks
- Files like `sensor/automation.h`, `climate/automation.h`
- Implement triggers like `on_value:`, `on_state:`
- Cannot be migrated - this is user-facing automation functionality
### 7. **Miscellaneous** (~5-10 uses)
- **voice_assistant/micro_wake_word:** State coordination
- **esp32_improv:** Provisioning state tracking
- **http_request/update:** Update status monitoring
- **switch/binary_sensor:** Cross-component dependencies
- **OTA callbacks:** OTA state monitoring
## Key Insights
### What's NOT Using Callbacks Anymore ✅
**API Server and WebServer** - migrated to Controller Registry
- **Before:** Each entity had 2 callbacks (API + WebServer) = ~32 bytes overhead
- **After:** Zero per-entity overhead = saves ~32 bytes per entity
### What SHOULD Keep Using Callbacks ✅
All the above categories have legitimate reasons:
1. **Per-entity configuration:** MQTT needs custom topics/QoS per entity
2. **Entity-to-entity relationships:** Copy components, derivative sensors
3. **Control loop feedback:** Climate/cover with external sensors
4. **User-defined automations:** YAML triggers configured by users
5. **Component dependencies:** Components that genuinely depend on other entities
## Memory Impact
**Per Sensor (ESP32):**
- Empty callback infrastructure: **~16 bytes** (unique_ptr + empty vector)
- With one callback (e.g., MQTT): **~32 bytes** (16 + std::function)
- With multiple callbacks: **~32 + 16n bytes** (where n = additional callbacks)
**Typical scenarios:**
- Sensor with **only API/WebServer:** ~16 bytes (no callbacks registered)
- Sensor with **MQTT:** ~32 bytes (one callback)
- Sensor with **MQTT + automation:** ~48 bytes (two callbacks)
- Sensor with **copy + total_daily_energy + graph:** ~64 bytes (three callbacks)
## Conclusion
The callback system is still heavily used (~103 occurrences) but for **appropriate reasons**:
- Components with per-entity state/configuration (MQTT, Copy)
- Sensor processing chains (derivatives, transformations)
- Control loops with external feedback (climate, covers)
- User-defined automations (cannot be removed)
The Controller Registry successfully eliminated wasteful callbacks for **stateless global handlers** (API/WebServer), saving ~32 bytes per entity for those use cases.
**No further callback elimination opportunities** exist without fundamentally changing ESPHome's architecture or breaking user-facing features.

View File

@@ -227,7 +227,6 @@ CONFIG_SCHEMA = cv.All(
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=16, # Abundant resources
ln882x=8, # Moderate RAM

View File

@@ -1467,8 +1467,6 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
#elif defined(USE_LN882X)
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
#elif defined(USE_NRF52)
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
#elif defined(USE_RTL87XX)
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
#elif defined(USE_HOST)

View File

@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
void CalibrationNumber::setup() {
float value = 0.0f;
if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
if (!this->pref_.load(&value)) {
value = 0.0f;
}

View File

@@ -15,7 +15,6 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_VALUE,
)
from esphome.core import ID
AUTO_LOAD = ["esp32_ble_client"]
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
@@ -199,12 +198,7 @@ async def ble_write_to_code(config, action_id, template_arg, args):
templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_value_template(templ))
else:
# Generate static array in flash to avoid RAM copy
if isinstance(value, bytes):
value = list(value)
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
cg.add(var.set_value_simple(arr, len(value)))
cg.add(var.set_value_simple(value))
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add(

View File

@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this);
ble_client_ = ble_client;
this->construct_simple_value_();
}
~BLEClientWriteAction() { this->destroy_simple_value_(); }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
@@ -107,14 +110,17 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->value_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
this->destroy_simple_value_();
this->value_.template_func = func;
this->has_simple_value_ = false;
}
// Store pointer to static data in flash (no RAM copy)
void set_value_simple(const uint8_t *data, size_t len) {
this->value_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_value_simple(const std::vector<uint8_t> &value) {
if (!this->has_simple_value_) {
this->construct_simple_value_();
}
this->value_.simple = value;
this->has_simple_value_ = true;
}
void play(const Ts &...x) override {}
@@ -122,14 +128,7 @@ 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...);
std::vector<uint8_t> value;
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
value.assign(this->value_.data, this->value_.data + this->len_);
} else {
// Template mode: call function
value = this->value_.func(x...);
}
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value))
this->play_next_(x...);
@@ -202,11 +201,21 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
}
private:
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
void destroy_simple_value_() {
if (this->has_simple_value_) {
this->value_.simple.~vector();
}
}
BLEClient *ble_client_;
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
bool has_simple_value_ = true;
union Value {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
std::vector<uint8_t> simple;
std::vector<uint8_t> (*template_func)(Ts...);
Value() {} // trivial constructor
~Value() {} // trivial destructor - we manage lifetime via discriminator
} value_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;

View File

@@ -4,7 +4,7 @@ from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
from esphome.core import CORE, ID
from esphome.core import CORE
CODEOWNERS = ["@mvturnho", "@danielschramm"]
IS_PLATFORM_COMPONENT = True
@@ -176,8 +176,5 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
else:
if isinstance(data, bytes):
data = [int(x) for x in data]
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
cg.add(var.set_data_static(data))
return var

View File

@@ -112,16 +112,13 @@ class Canbus : public Component {
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
this->data_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
void set_data_template(const std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
// Store pointer to static data in flash (no RAM copy)
void set_data_static(const uint8_t *data, size_t len) {
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
@@ -136,26 +133,21 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
std::vector<uint8_t> data;
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
data.assign(this->data_.data, this->data_.data + this->len_);
if (this->static_) {
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
} else {
// Template mode: call function
data = this->data_.func(x...);
auto val = this->data_func_(x...);
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
}
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data);
}
protected:
optional<uint32_t> can_id_{};
optional<bool> use_extended_id_{};
bool remote_transmission_request_{false};
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} data_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {

View File

@@ -59,7 +59,6 @@ async def to_code(config):
zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add_define("USE_DEBUG")
FILTER_SOURCE_FILES = filter_source_files_from_platform(

View File

@@ -1,247 +0,0 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import (
CONF_DIRECTION,
CONF_ID,
CONF_NAME,
CONF_ON_ENROLLMENT_DONE,
CONF_ON_ENROLLMENT_FAILED,
CONF_TRIGGER_ID,
)
CODEOWNERS = ["@OnFreund"]
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"]
MULTI_CONF = True
CONF_HLK_FM22X_ID = "hlk_fm22x_id"
CONF_FACE_ID = "face_id"
CONF_ON_FACE_SCAN_MATCHED = "on_face_scan_matched"
CONF_ON_FACE_SCAN_UNMATCHED = "on_face_scan_unmatched"
CONF_ON_FACE_SCAN_INVALID = "on_face_scan_invalid"
CONF_ON_FACE_INFO = "on_face_info"
hlk_fm22x_ns = cg.esphome_ns.namespace("hlk_fm22x")
HlkFm22xComponent = hlk_fm22x_ns.class_(
"HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice
)
FaceScanMatchedTrigger = hlk_fm22x_ns.class_(
"FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string)
)
FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_(
"FaceScanUnmatchedTrigger", automation.Trigger.template()
)
FaceScanInvalidTrigger = hlk_fm22x_ns.class_(
"FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8)
)
FaceInfoTrigger = hlk_fm22x_ns.class_(
"FaceInfoTrigger",
automation.Trigger.template(
cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16
),
)
EnrollmentDoneTrigger = hlk_fm22x_ns.class_(
"EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8)
)
EnrollmentFailedTrigger = hlk_fm22x_ns.class_(
"EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8)
)
EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action)
DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action)
DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action)
ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action)
ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HlkFm22xComponent),
cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FaceScanMatchedTrigger
),
}
),
cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FaceScanUnmatchedTrigger
),
}
),
cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FaceScanInvalidTrigger
),
}
),
cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger),
}
),
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentDoneTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
EnrollmentFailedTrigger
),
}
),
}
)
.extend(cv.polling_component_schema("50ms"))
.extend(uart.UART_DEVICE_SCHEMA),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf
)
for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
for conf in config.get(CONF_ON_FACE_INFO, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
[
(cg.int16, "status"),
(cg.int16, "left"),
(cg.int16, "top"),
(cg.int16, "right"),
(cg.int16, "bottom"),
(cg.int16, "yaw"),
(cg.int16, "pitch"),
(cg.int16, "roll"),
],
conf,
)
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf
)
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
@automation.register_action(
"hlk_fm22x.enroll",
EnrollmentAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
cv.Required(CONF_NAME): cv.templatable(cv.string),
cv.Required(CONF_DIRECTION): cv.templatable(cv.uint8_t),
},
key=CONF_NAME,
),
)
async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_NAME], args, cg.std_string)
cg.add(var.set_name(template_))
template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8)
cg.add(var.set_direction(template_))
return var
@automation.register_action(
"hlk_fm22x.delete",
DeleteAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t),
},
key=CONF_FACE_ID,
),
)
async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FACE_ID], args, cg.int16)
cg.add(var.set_face_id(template_))
return var
@automation.register_action(
"hlk_fm22x.delete_all",
DeleteAllAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
)
async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"hlk_fm22x.scan",
ScanAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
)
async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"hlk_fm22x.reset",
ResetAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
)
async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -1,21 +0,0 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ICON, ICON_KEY_PLUS
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
DEPENDENCIES = ["hlk_fm22x"]
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
{
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon,
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
var = await binary_sensor.new_binary_sensor(config)
cg.add(hub.set_enrolling_binary_sensor(var))

View File

@@ -1,325 +0,0 @@
#include "hlk_fm22x.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <array>
#include <cinttypes>
namespace esphome::hlk_fm22x {
static const char *const TAG = "hlk_fm22x";
void HlkFm22xComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
this->set_enrolling_(false);
while (this->available()) {
this->read();
}
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
}
void HlkFm22xComponent::update() {
if (this->active_command_ != HlkFm22xCommand::NONE) {
if (this->wait_cycles_ > 600) {
ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
if (HlkFm22xCommand::RESET == this->active_command_) {
this->mark_failed();
} else {
this->reset();
}
}
}
this->recv_command_();
}
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
if (name.length() > 31) {
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
return;
}
ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
std::array<uint8_t, 35> data{};
data[0] = 0; // admin
std::copy(name.begin(), name.end(), data.begin() + 1);
// Remaining bytes are already zero-initialized
data[33] = (uint8_t) direction;
data[34] = 10; // timeout
this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
this->set_enrolling_(true);
}
void HlkFm22xComponent::scan_face() {
ESP_LOGI(TAG, "Verify face");
static const uint8_t DATA[] = {0, 0};
this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
}
void HlkFm22xComponent::delete_face(int16_t face_id) {
ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
}
void HlkFm22xComponent::delete_all_faces() {
ESP_LOGI(TAG, "Deleting all stored faces");
this->send_command_(HlkFm22xCommand::DELETE_ALL_FACES);
}
void HlkFm22xComponent::get_face_count_() {
ESP_LOGD(TAG, "Getting face count");
this->send_command_(HlkFm22xCommand::GET_ALL_FACE_IDS);
}
void HlkFm22xComponent::reset() {
ESP_LOGI(TAG, "Resetting module");
this->active_command_ = HlkFm22xCommand::NONE;
this->wait_cycles_ = 0;
this->set_enrolling_(false);
this->send_command_(HlkFm22xCommand::RESET);
}
void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
ESP_LOGV(TAG, "Send command: 0x%.2X", command);
if (this->active_command_ != HlkFm22xCommand::NONE) {
ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
return;
}
this->wait_cycles_ = 0;
this->active_command_ = command;
while (this->available())
this->read();
this->write((uint8_t) (START_CODE >> 8));
this->write((uint8_t) (START_CODE & 0xFF));
this->write((uint8_t) command);
uint16_t data_size = size;
this->write((uint8_t) (data_size >> 8));
this->write((uint8_t) (data_size & 0xFF));
uint8_t checksum = 0;
checksum ^= (uint8_t) command;
checksum ^= (data_size >> 8);
checksum ^= (data_size & 0xFF);
for (size_t i = 0; i < size; i++) {
this->write(data[i]);
checksum ^= data[i];
}
this->write(checksum);
this->active_command_ = command;
this->wait_cycles_ = 0;
}
void HlkFm22xComponent::recv_command_() {
uint8_t byte, checksum = 0;
uint16_t length = 0;
if (this->available() < 7) {
++this->wait_cycles_;
return;
}
this->wait_cycles_ = 0;
if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
ESP_LOGE(TAG, "Invalid start code");
return;
}
byte = this->read();
checksum ^= byte;
HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
byte = this->read();
checksum ^= byte;
length = byte << 8;
byte = this->read();
checksum ^= byte;
length |= byte;
std::vector<uint8_t> data;
data.reserve(length);
for (uint16_t idx = 0; idx < length; ++idx) {
byte = this->read();
checksum ^= byte;
data.push_back(byte);
}
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str());
byte = this->read();
if (byte != checksum) {
ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
return;
}
switch (response_type) {
case HlkFm22xResponseType::NOTE:
this->handle_note_(data);
break;
case HlkFm22xResponseType::REPLY:
this->handle_reply_(data);
break;
default:
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
break;
}
}
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
switch (data[0]) {
case HlkFm22xNoteType::FACE_STATE:
if (data.size() < 17) {
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
break;
}
{
int16_t info[8];
uint8_t offset = 1;
for (int16_t &i : info) {
i = ((int16_t) data[offset + 1] << 8) | data[offset];
offset += 2;
}
ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
}
break;
case HlkFm22xNoteType::READY:
ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
switch (this->active_command_) {
case HlkFm22xCommand::ENROLL:
this->set_enrolling_(false);
this->enrollment_failed_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT);
break;
case HlkFm22xCommand::VERIFY:
this->face_scan_invalid_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT);
break;
default:
break;
}
this->active_command_ = HlkFm22xCommand::NONE;
this->wait_cycles_ = 0;
break;
default:
ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
break;
}
}
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
auto expected = this->active_command_;
this->active_command_ = HlkFm22xCommand::NONE;
if (data[0] != (uint8_t) expected) {
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
return;
}
if (data[1] != HlkFm22xResult::SUCCESS) {
ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
switch (expected) {
case HlkFm22xCommand::ENROLL:
this->set_enrolling_(false);
this->enrollment_failed_callback_.call(data[1]);
break;
case HlkFm22xCommand::VERIFY:
if (data[1] == HlkFm22xResult::REJECTED) {
this->face_scan_unmatched_callback_.call();
} else {
this->face_scan_invalid_callback_.call(data[1]);
}
break;
default:
break;
}
return;
}
switch (expected) {
case HlkFm22xCommand::VERIFY: {
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
std::string name(data.begin() + 4, data.begin() + 36);
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
if (this->last_face_id_sensor_ != nullptr) {
this->last_face_id_sensor_->publish_state(face_id);
}
if (this->last_face_name_text_sensor_ != nullptr) {
this->last_face_name_text_sensor_->publish_state(name);
}
this->face_scan_matched_callback_.call(face_id, name);
break;
}
case HlkFm22xCommand::ENROLL: {
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
HlkFm22xFaceDirection direction = (HlkFm22xFaceDirection) data[4];
ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
this->set_enrolling_(false);
this->defer([this]() { this->get_face_count_(); });
break;
}
case HlkFm22xCommand::GET_STATUS:
if (this->status_sensor_ != nullptr) {
this->status_sensor_->publish_state(data[2]);
}
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
break;
case HlkFm22xCommand::GET_VERSION:
if (this->version_text_sensor_ != nullptr) {
std::string version(data.begin() + 2, data.end());
this->version_text_sensor_->publish_state(version);
}
this->defer([this]() { this->get_face_count_(); });
break;
case HlkFm22xCommand::GET_ALL_FACE_IDS:
if (this->face_count_sensor_ != nullptr) {
this->face_count_sensor_->publish_state(data[2]);
}
break;
case HlkFm22xCommand::DELETE_FACE:
ESP_LOGI(TAG, "Deleted face");
break;
case HlkFm22xCommand::DELETE_ALL_FACES:
ESP_LOGI(TAG, "Deleted all faces");
break;
case HlkFm22xCommand::RESET:
ESP_LOGI(TAG, "Module reset");
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
break;
default:
ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
break;
}
}
void HlkFm22xComponent::set_enrolling_(bool enrolling) {
if (this->enrolling_binary_sensor_ != nullptr) {
this->enrolling_binary_sensor_->publish_state(enrolling);
}
}
void HlkFm22xComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HLK_FM22X:");
LOG_UPDATE_INTERVAL(this);
if (this->version_text_sensor_) {
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
}
if (this->enrolling_binary_sensor_) {
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
}
if (this->face_count_sensor_) {
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
}
if (this->status_sensor_) {
LOG_SENSOR(" ", "Status", this->status_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
}
if (this->last_face_id_sensor_) {
LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
}
if (this->last_face_name_text_sensor_) {
LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
}
}
} // namespace esphome::hlk_fm22x

View File

@@ -1,224 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h"
#include <utility>
#include <vector>
namespace esphome::hlk_fm22x {
static const uint16_t START_CODE = 0xEFAA;
enum HlkFm22xCommand {
NONE = 0x00,
RESET = 0x10,
GET_STATUS = 0x11,
VERIFY = 0x12,
ENROLL = 0x13,
DELETE_FACE = 0x20,
DELETE_ALL_FACES = 0x21,
GET_ALL_FACE_IDS = 0x24,
GET_VERSION = 0x30,
GET_SERIAL_NUMBER = 0x93,
};
enum HlkFm22xResponseType {
REPLY = 0x00,
NOTE = 0x01,
IMAGE = 0x02,
};
enum HlkFm22xNoteType {
READY = 0x00,
FACE_STATE = 0x01,
};
enum HlkFm22xResult {
SUCCESS = 0x00,
REJECTED = 0x01,
ABORTED = 0x02,
FAILED4_CAMERA = 0x04,
FAILED4_UNKNOWNREASON = 0x05,
FAILED4_INVALIDPARAM = 0x06,
FAILED4_NOMEMORY = 0x07,
FAILED4_UNKNOWNUSER = 0x08,
FAILED4_MAXUSER = 0x09,
FAILED4_FACEENROLLED = 0x0A,
FAILED4_LIVENESSCHECK = 0x0C,
FAILED4_TIMEOUT = 0x0D,
FAILED4_AUTHORIZATION = 0x0E,
FAILED4_READ_FILE = 0x13,
FAILED4_WRITE_FILE = 0x14,
FAILED4_NO_ENCRYPT = 0x15,
FAILED4_NO_RGBIMAGE = 0x17,
FAILED4_JPGPHOTO_LARGE = 0x18,
FAILED4_JPGPHOTO_SMALL = 0x19,
};
enum HlkFm22xFaceDirection {
FACE_DIRECTION_UNDEFINED = 0x00,
FACE_DIRECTION_MIDDLE = 0x01,
FACE_DIRECTION_RIGHT = 0x02,
FACE_DIRECTION_LEFT = 0x04,
FACE_DIRECTION_DOWN = 0x08,
FACE_DIRECTION_UP = 0x10,
};
class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
void set_face_count_sensor(sensor::Sensor *face_count_sensor) { this->face_count_sensor_ = face_count_sensor; }
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
void set_last_face_id_sensor(sensor::Sensor *last_face_id_sensor) {
this->last_face_id_sensor_ = last_face_id_sensor;
}
void set_last_face_name_text_sensor(text_sensor::TextSensor *last_face_name_text_sensor) {
this->last_face_name_text_sensor_ = last_face_name_text_sensor;
}
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
this->enrolling_binary_sensor_ = enrolling_binary_sensor;
}
void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) {
this->version_text_sensor_ = version_text_sensor;
}
void add_on_face_scan_matched_callback(std::function<void(int16_t, std::string)> callback) {
this->face_scan_matched_callback_.add(std::move(callback));
}
void add_on_face_scan_unmatched_callback(std::function<void()> callback) {
this->face_scan_unmatched_callback_.add(std::move(callback));
}
void add_on_face_scan_invalid_callback(std::function<void(uint8_t)> callback) {
this->face_scan_invalid_callback_.add(std::move(callback));
}
void add_on_face_info_callback(
std::function<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> callback) {
this->face_info_callback_.add(std::move(callback));
}
void add_on_enrollment_done_callback(std::function<void(int16_t, uint8_t)> callback) {
this->enrollment_done_callback_.add(std::move(callback));
}
void add_on_enrollment_failed_callback(std::function<void(uint8_t)> callback) {
this->enrollment_failed_callback_.add(std::move(callback));
}
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction);
void scan_face();
void delete_face(int16_t face_id);
void delete_all_faces();
void reset();
protected:
void get_face_count_();
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
void recv_command_();
void handle_note_(const std::vector<uint8_t> &data);
void handle_reply_(const std::vector<uint8_t> &data);
void set_enrolling_(bool enrolling);
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
uint16_t wait_cycles_ = 0;
sensor::Sensor *face_count_sensor_{nullptr};
sensor::Sensor *status_sensor_{nullptr};
sensor::Sensor *last_face_id_sensor_{nullptr};
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
text_sensor::TextSensor *last_face_name_text_sensor_{nullptr};
text_sensor::TextSensor *version_text_sensor_{nullptr};
CallbackManager<void(uint8_t)> face_scan_invalid_callback_;
CallbackManager<void(int16_t, std::string)> face_scan_matched_callback_;
CallbackManager<void()> face_scan_unmatched_callback_;
CallbackManager<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> face_info_callback_;
CallbackManager<void(int16_t, uint8_t)> enrollment_done_callback_;
CallbackManager<void(uint8_t)> enrollment_failed_callback_;
};
class FaceScanMatchedTrigger : public Trigger<int16_t, std::string> {
public:
explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_scan_matched_callback(
[this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); });
}
};
class FaceScanUnmatchedTrigger : public Trigger<> {
public:
explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); });
}
};
class FaceScanInvalidTrigger : public Trigger<uint8_t> {
public:
explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); });
}
};
class FaceInfoTrigger : public Trigger<int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t> {
public:
explicit FaceInfoTrigger(HlkFm22xComponent *parent) {
parent->add_on_face_info_callback(
[this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch,
int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); });
}
};
class EnrollmentDoneTrigger : public Trigger<int16_t, uint8_t> {
public:
explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) {
parent->add_on_enrollment_done_callback(
[this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); });
}
};
class EnrollmentFailedTrigger : public Trigger<uint8_t> {
public:
explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) {
parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); });
}
};
template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
TEMPLATABLE_VALUE(std::string, name)
TEMPLATABLE_VALUE(uint8_t, direction)
void play(Ts... x) override {
auto name = this->name_.value(x...);
auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...);
this->parent_->enroll_face(name, direction);
}
};
template<typename... Ts> class DeleteAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
TEMPLATABLE_VALUE(int16_t, face_id)
void play(Ts... x) override {
auto face_id = this->face_id_.value(x...);
this->parent_->delete_face(face_id);
}
};
template<typename... Ts> class DeleteAllAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(Ts... x) override { this->parent_->delete_all_faces(); }
};
template<typename... Ts> class ScanAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(Ts... x) override { this->parent_->scan_face(); }
};
template<typename... Ts> class ResetAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
public:
void play(Ts... x) override { this->parent_->reset(); }
};
} // namespace esphome::hlk_fm22x

View File

@@ -1,47 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
DEPENDENCIES = ["hlk_fm22x"]
CONF_FACE_COUNT = "face_count"
CONF_LAST_FACE_ID = "last_face_id"
ICON_FACE = "mdi:face-recognition"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
cv.Optional(CONF_FACE_COUNT): sensor.sensor_schema(
icon=ICON_FACE,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_STATUS): sensor.sensor_schema(
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_LAST_FACE_ID): sensor.sensor_schema(
icon=ICON_ACCOUNT,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
for key in [
CONF_FACE_COUNT,
CONF_STATUS,
CONF_LAST_FACE_ID,
]:
if key not in config:
continue
conf = config[key]
sens = await sensor.new_sensor(conf)
cg.add(getattr(hub, f"set_{key}_sensor")(sens))

View File

@@ -1,42 +0,0 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_VERSION,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_ACCOUNT,
ICON_RESTART,
)
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
DEPENDENCIES = ["hlk_fm22x"]
CONF_LAST_FACE_NAME = "last_face_name"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
icon=ICON_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_LAST_FACE_NAME): text_sensor.text_sensor_schema(
icon=ICON_ACCOUNT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
for key in [
CONF_VERSION,
CONF_LAST_FACE_NAME,
]:
if key not in config:
continue
conf = config[key]
sens = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))

View File

@@ -1,7 +1,7 @@
#pragma once
#include <climits>
#include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf
namespace esphome {
namespace hm3301 {
@@ -16,15 +16,16 @@ class AQICalculator : public AbstractAQICalculator {
}
protected:
static const int AMOUNT_OF_LEVELS = 6;
static const int AMOUNT_OF_LEVELS = 7;
int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200},
{201, 300}, {301, 400}, {401, 500}};
int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55},
{56, 125}, {126, 225}, {226, INT_MAX}};
int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150},
{151, 250}, {251, 350}, {351, 500}};
int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254},
{255, 354}, {355, 424}, {425, INT_MAX}};
int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354},
{355, 424}, {425, 504}, {505, 604}};
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
int grid_index = get_grid_index_(value, array);

View File

@@ -39,7 +39,7 @@ from esphome.const import (
CONF_WAND_ID,
CONF_ZERO,
)
from esphome.core import ID, coroutine
from esphome.core import coroutine
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.util import Registry, SimpleRegistry
@@ -2104,9 +2104,7 @@ async def abbwelcome_action(var, config, args):
)
cg.add(var.set_data_template(template_))
else:
arr_id = ID(f"{var.base}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data_))
cg.add(var.set_data_static(arr, len(data_)))
cg.add(var.set_data_static(data_))
# Mirage

View File

@@ -214,13 +214,10 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
TEMPLATABLE_VALUE(uint8_t, message_type)
TEMPLATABLE_VALUE(uint8_t, message_id)
TEMPLATABLE_VALUE(bool, auto_message_id)
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
}
void set_data_static(const uint8_t *data, size_t len) {
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(std::vector<uint8_t> data) { data_static_ = std::move(data); }
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
has_data_func_ = true;
}
void encode(RemoteTransmitData *dst, Ts... x) override {
ABBWelcomeData data;
@@ -231,25 +228,19 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
data.set_message_type(this->message_type_.value(x...));
data.set_message_id(this->message_id_.value(x...));
data.auto_message_id = this->auto_message_id_.value(x...);
std::vector<uint8_t> data_vec;
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
data_vec.assign(this->data_.data, this->data_.data + this->len_);
if (has_data_func_) {
data.set_data(this->data_func_(x...));
} else {
// Template mode: call function
data_vec = this->data_.func(x...);
data.set_data(this->data_static_);
}
data.set_data(data_vec);
data.finalize();
ABBWelcomeProtocol().encode(dst, data);
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} data_;
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
bool has_data_func_{false};
};
} // namespace remote_base

View File

@@ -71,7 +71,6 @@ static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0
static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL;
static const uint16_t PRONTO_DEFAULT_GAP = 45000;
static const uint16_t MARK_EXCESS_MICROS = 20;
static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230;
static uint16_t to_frequency_k_hz(uint16_t code) {
if (code == 0)
@@ -226,18 +225,18 @@ optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
}
void ProntoProtocol::dump(const ProntoData &data) {
std::string rest;
rest = data.data;
ESP_LOGI(TAG, "Received Pronto: data=");
const char *ptr = data.data.c_str();
size_t remaining = data.data.size();
// Log in chunks, always logging at least once (even for empty string)
do {
size_t chunk_size = remaining < PRONTO_LOG_CHUNK_SIZE ? remaining : PRONTO_LOG_CHUNK_SIZE;
ESP_LOGI(TAG, "%.*s", (int) chunk_size, ptr);
ptr += chunk_size;
remaining -= chunk_size;
} while (remaining > 0);
while (true) {
ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str());
if (rest.size() > 230) {
rest = rest.substr(230);
} else {
break;
}
}
}
} // namespace remote_base

View File

@@ -42,20 +42,17 @@ class RawTrigger : public Trigger<RawTimings>, public Component, public RemoteRe
template<typename... Ts> class RawAction : public RemoteTransmitterActionBase<Ts...> {
public:
void set_code_template(RawTimings (*func)(Ts...)) {
this->code_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
}
void set_code_template(std::function<RawTimings(Ts...)> func) { this->code_func_ = func; }
void set_code_static(const int32_t *code, size_t len) {
this->code_.data = code;
this->len_ = len; // Length >= 0 indicates static mode
this->code_static_ = code;
this->code_static_len_ = len;
}
TEMPLATABLE_VALUE(uint32_t, carrier_frequency);
void encode(RemoteTransmitData *dst, Ts... x) override {
if (this->len_ >= 0) {
for (size_t i = 0; i < static_cast<size_t>(this->len_); i++) {
auto val = this->code_.data[i];
if (this->code_static_ != nullptr) {
for (size_t i = 0; i < this->code_static_len_; i++) {
auto val = this->code_static_[i];
if (val < 0) {
dst->space(static_cast<uint32_t>(-val));
} else {
@@ -63,17 +60,15 @@ template<typename... Ts> class RawAction : public RemoteTransmitterActionBase<Ts
}
}
} else {
dst->set_data(this->code_.func(x...));
dst->set_data(this->code_func_(x...));
}
dst->set_carrier_frequency(this->carrier_frequency_.value(x...));
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Code {
RawTimings (*func)(Ts...);
const int32_t *data;
} code_;
std::function<RawTimings(Ts...)> code_func_{nullptr};
const int32_t *code_static_{nullptr};
int32_t code_static_len_{0};
};
class RawDumper : public RemoteReceiverDumperBase {

View File

@@ -3,7 +3,7 @@ import esphome.codegen as cg
from esphome.components import audio, audio_dac
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME
from esphome.core import CORE, ID
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
AUTO_LOAD = ["audio"]
@@ -90,10 +90,7 @@ async def speaker_play_action(config, action_id, template_arg, args):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
cg.add(var.set_data_static(data))
return var

View File

@@ -10,33 +10,28 @@ namespace speaker {
template<typename... Ts> class PlayAction : public Action<Ts...>, public Parented<Speaker> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const uint8_t *data, size_t len) {
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(const Ts &...x) override {
if (this->len_ >= 0) {
// Static mode: pass pointer directly to play(const uint8_t *, size_t)
this->parent_->play(this->data_.data, static_cast<size_t>(this->len_));
if (this->static_) {
this->parent_->play(this->data_static_);
} else {
// Template mode: call function and pass vector to play(const std::vector<uint8_t> &)
auto val = this->data_.func(x...);
auto val = this->data_func_(x...);
this->parent_->play(val);
}
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} data_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<Speaker> {

View File

@@ -3,7 +3,7 @@ import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID, TimePeriod
from esphome.core import TimePeriod
MULTI_CONF = True
CODEOWNERS = ["@swoboda1337"]
@@ -189,7 +189,7 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.declare_id(SX126x),
cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000),
cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_CRC_INVERTED, default=True): cv.boolean,
@@ -201,7 +201,7 @@ CONFIG_SCHEMA = (
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
@@ -213,7 +213,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4),
cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535),
cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_RX_START, default=True): cv.boolean,
cv.Required(CONF_RF_SWITCH): cv.boolean,
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
@@ -329,8 +329,5 @@ async def send_packet_action_to_code(config, action_id, template_arg, args):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
cg.add(var.set_data_static(data))
return var

View File

@@ -14,34 +14,28 @@ template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX126x> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const uint8_t *data, size_t len) {
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(const Ts &...x) override {
std::vector<uint8_t> data;
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
data.assign(this->data_.data, this->data_.data + this->len_);
if (this->static_) {
this->parent_->transmit_packet(this->data_static_);
} else {
// Template mode: call function
data = this->data_.func(x...);
this->parent_->transmit_packet(this->data_func_(x...));
}
this->parent_->transmit_packet(data);
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} data_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX126x> {

View File

@@ -64,7 +64,7 @@ class SX126x : public Component,
void dump_config() override;
void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; }
@@ -72,7 +72,7 @@ class SX126x : public Component,
void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; }
void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; }
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; }
void set_mode_rx();
@@ -85,7 +85,7 @@ class SX126x : public Component,
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
@@ -115,9 +115,9 @@ class SX126x : public Component,
std::vector<SX126xListener *> listeners_;
std::vector<uint8_t> packet_;
std::vector<uint8_t> sync_value_;
GPIOPin *busy_pin_{nullptr};
GPIOPin *dio1_pin_{nullptr};
GPIOPin *rst_pin_{nullptr};
InternalGPIOPin *busy_pin_{nullptr};
InternalGPIOPin *dio1_pin_{nullptr};
InternalGPIOPin *rst_pin_{nullptr};
std::string hw_version_;
char version_[16];
SX126xBw bandwidth_{SX126X_BW_125000};

View File

@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
from esphome.core import ID
MULTI_CONF = True
CODEOWNERS = ["@swoboda1337"]
@@ -322,8 +321,5 @@ async def send_packet_action_to_code(config, action_id, template_arg, args):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
cg.add(var.set_data_static(data))
return var

View File

@@ -14,34 +14,28 @@ template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX127x> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const uint8_t *data, size_t len) {
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(const Ts &...x) override {
std::vector<uint8_t> data;
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
data.assign(this->data_.data, this->data_.data + this->len_);
if (this->static_) {
this->parent_->transmit_packet(this->data_static_);
} else {
// Template mode: call function
data = this->data_.func(x...);
this->parent_->transmit_packet(this->data_func_(x...));
}
this->parent_->transmit_packet(data);
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} data_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX127x> {

View File

@@ -31,7 +31,7 @@ from esphome.const import (
PLATFORM_HOST,
PlatformFramework,
)
from esphome.core import CORE, ID
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.yaml_util import make_data_base
@@ -446,10 +446,7 @@ async def uart_write_to_code(config, action_id, template_arg, args):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
cg.add(var.set_data_static(cg.ArrayInitializer(*data)))
return var

View File

@@ -10,35 +10,32 @@ namespace uart {
template<typename... Ts> class UARTWriteAction : public Action<Ts...>, public Parented<UARTComponent> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
this->code_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
// Store pointer to static data in flash (no RAM copy)
void set_data_static(const uint8_t *data, size_t len) {
this->code_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(std::vector<uint8_t> &&data) {
this->data_static_ = std::move(data);
this->static_ = true;
}
void set_data_static(std::initializer_list<uint8_t> data) {
this->data_static_ = std::vector<uint8_t>(data);
this->static_ = true;
}
void play(const Ts &...x) override {
if (this->len_ >= 0) {
// Static mode: use pointer and length
this->parent_->write_array(this->code_.data, static_cast<size_t>(this->len_));
if (this->static_) {
this->parent_->write_array(this->data_static_);
} else {
// Template mode: call function
auto val = this->code_.func(x...);
auto val = this->data_func_(x...);
this->parent_->write_array(val);
}
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Code {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} code_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
} // namespace uart

View File

@@ -12,7 +12,7 @@ from esphome.components.packet_transport import (
)
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID, Lambda
from esphome.core import Lambda
from esphome.cpp_generator import ExpressionStatement, MockObj
CODEOWNERS = ["@clydebarrow"]
@@ -158,8 +158,5 @@ async def udp_write_to_code(config, action_id, template_arg, args):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
cg.add(var.set_data_static(data))
return var

View File

@@ -11,33 +11,28 @@ namespace udp {
template<typename... Ts> class UDPWriteAction : public Action<Ts...>, public Parented<UDPComponent> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
this->data_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const uint8_t *data, size_t len) {
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(const Ts &...x) override {
if (this->len_ >= 0) {
// Static mode: pass pointer directly to send_packet(const uint8_t *, size_t)
this->parent_->send_packet(this->data_.data, static_cast<size_t>(this->len_));
if (this->static_) {
this->parent_->send_packet(this->data_static_);
} else {
// Template mode: call function and pass vector to send_packet(const std::vector<uint8_t> &)
auto val = this->data_.func(x...);
auto val = this->data_func_(x...);
this->parent_->send_packet(val);
}
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Data {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} data_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
} // namespace udp

View File

@@ -6,7 +6,6 @@
#include <zephyr/random/random.h>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
namespace esphome {
@@ -26,14 +25,7 @@ void arch_init() {
wdt_config.window.max = 2000;
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
if (wdt_channel_id >= 0) {
uint8_t options = 0;
#ifdef USE_DEBUG
options |= WDT_OPT_PAUSE_HALTED_BY_DBG;
#endif
#ifdef USE_DEEP_SLEEP
options |= WDT_OPT_PAUSE_IN_SLEEP;
#endif
wdt_setup(WDT, options);
wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP);
}
}
}

View File

@@ -10,105 +10,166 @@ StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controll
void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); }
// Macro for standard registry notification dispatch - calls on_<entity_name>_update()
#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \
void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
for (auto *controller : controllers) { \
controller->on_##entity_name##_update(obj); \
} \
}
// Macro for entities where controller method has no "_update" suffix (Event, Update)
#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \
void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
for (auto *controller : controllers) { \
controller->on_##entity_name(obj); \
} \
}
#ifdef USE_BINARY_SENSOR
CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor)
void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor *obj) {
for (auto *controller : controllers) {
controller->on_binary_sensor_update(obj);
}
}
#endif
#ifdef USE_FAN
CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan)
void ControllerRegistry::notify_fan_update(fan::Fan *obj) {
for (auto *controller : controllers) {
controller->on_fan_update(obj);
}
}
#endif
#ifdef USE_LIGHT
CONTROLLER_REGISTRY_NOTIFY(light::LightState, light)
void ControllerRegistry::notify_light_update(light::LightState *obj) {
for (auto *controller : controllers) {
controller->on_light_update(obj);
}
}
#endif
#ifdef USE_SENSOR
CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor)
void ControllerRegistry::notify_sensor_update(sensor::Sensor *obj) {
for (auto *controller : controllers) {
controller->on_sensor_update(obj);
}
}
#endif
#ifdef USE_SWITCH
CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch)
void ControllerRegistry::notify_switch_update(switch_::Switch *obj) {
for (auto *controller : controllers) {
controller->on_switch_update(obj);
}
}
#endif
#ifdef USE_COVER
CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover)
void ControllerRegistry::notify_cover_update(cover::Cover *obj) {
for (auto *controller : controllers) {
controller->on_cover_update(obj);
}
}
#endif
#ifdef USE_TEXT_SENSOR
CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor)
void ControllerRegistry::notify_text_sensor_update(text_sensor::TextSensor *obj) {
for (auto *controller : controllers) {
controller->on_text_sensor_update(obj);
}
}
#endif
#ifdef USE_CLIMATE
CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate)
void ControllerRegistry::notify_climate_update(climate::Climate *obj) {
for (auto *controller : controllers) {
controller->on_climate_update(obj);
}
}
#endif
#ifdef USE_NUMBER
CONTROLLER_REGISTRY_NOTIFY(number::Number, number)
void ControllerRegistry::notify_number_update(number::Number *obj) {
for (auto *controller : controllers) {
controller->on_number_update(obj);
}
}
#endif
#ifdef USE_DATETIME_DATE
CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date)
void ControllerRegistry::notify_date_update(datetime::DateEntity *obj) {
for (auto *controller : controllers) {
controller->on_date_update(obj);
}
}
#endif
#ifdef USE_DATETIME_TIME
CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time)
void ControllerRegistry::notify_time_update(datetime::TimeEntity *obj) {
for (auto *controller : controllers) {
controller->on_time_update(obj);
}
}
#endif
#ifdef USE_DATETIME_DATETIME
CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime)
void ControllerRegistry::notify_datetime_update(datetime::DateTimeEntity *obj) {
for (auto *controller : controllers) {
controller->on_datetime_update(obj);
}
}
#endif
#ifdef USE_TEXT
CONTROLLER_REGISTRY_NOTIFY(text::Text, text)
void ControllerRegistry::notify_text_update(text::Text *obj) {
for (auto *controller : controllers) {
controller->on_text_update(obj);
}
}
#endif
#ifdef USE_SELECT
CONTROLLER_REGISTRY_NOTIFY(select::Select, select)
void ControllerRegistry::notify_select_update(select::Select *obj) {
for (auto *controller : controllers) {
controller->on_select_update(obj);
}
}
#endif
#ifdef USE_LOCK
CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock)
void ControllerRegistry::notify_lock_update(lock::Lock *obj) {
for (auto *controller : controllers) {
controller->on_lock_update(obj);
}
}
#endif
#ifdef USE_VALVE
CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve)
void ControllerRegistry::notify_valve_update(valve::Valve *obj) {
for (auto *controller : controllers) {
controller->on_valve_update(obj);
}
}
#endif
#ifdef USE_MEDIA_PLAYER
CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player)
void ControllerRegistry::notify_media_player_update(media_player::MediaPlayer *obj) {
for (auto *controller : controllers) {
controller->on_media_player_update(obj);
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
for (auto *controller : controllers) {
controller->on_alarm_control_panel_update(obj);
}
}
#endif
#ifdef USE_EVENT
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event)
void ControllerRegistry::notify_event(event::Event *obj) {
for (auto *controller : controllers) {
controller->on_event(obj);
}
}
#endif
#ifdef USE_UPDATE
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update)
void ControllerRegistry::notify_update(update::UpdateEntity *obj) {
for (auto *controller : controllers) {
controller->on_update(obj);
}
}
#endif
#undef CONTROLLER_REGISTRY_NOTIFY
#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY

View File

@@ -34,7 +34,6 @@
#define USE_DATETIME_DATE
#define USE_DATETIME_DATETIME
#define USE_DATETIME_TIME
#define USE_DEBUG
#define USE_DEEP_SLEEP
#define USE_DEVICES
#define USE_DISPLAY

View File

@@ -129,6 +129,9 @@ class EntityBase {
// Returns empty StringRef if object_id is dynamic (needs allocation)
StringRef get_object_id_ref_for_api_() const;
/// The hash_base() function has been deprecated. It is kept in this
/// class for now, to prevent external components from not compiling.
virtual uint32_t hash_base() { return 0L; }
void calc_object_id_();
/// Check if the object_id is dynamic (changes with MAC suffix)

View File

@@ -0,0 +1,188 @@
# Lazy Callback Allocation - Tradeoff Analysis
## Current Implementation
```cpp
class Sensor {
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // lazy
CallbackManager<void(float)> callback_; // always allocated
};
```
## Proposed Implementation
```cpp
class Sensor {
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // lazy
std::unique_ptr<CallbackManager<void(float)>> callback_; // ALSO lazy
};
```
## Memory Impact (ESP32 32-bit)
### No Callbacks Registered
**Current:**
- `raw_callback_` unique_ptr: 4 bytes (nullptr)
- `callback_` vector struct: 12 bytes (empty, no heap allocation)
- **Total: 16 bytes**
**Lazy:**
- `raw_callback_` unique_ptr: 4 bytes (nullptr)
- `callback_` unique_ptr: 4 bytes (nullptr)
- **Total: 8 bytes**
**Savings: 8 bytes per entity without callbacks**
### One Callback Registered (e.g., MQTT)
**Current:**
- In object: 4 bytes (raw ptr) + 12 bytes (vector struct) = 16 bytes
- On heap: vector allocates storage for std::function ≈ 16 bytes
- **Total: 16 + 16 = 32 bytes**
**Lazy:**
- In object: 4 bytes (raw ptr) + 4 bytes (callback ptr) = 8 bytes
- Heap #1: CallbackManager object (vector struct) = 12 bytes
- Heap #2: vector allocates storage for std::function ≈ 16 bytes
- **Total: 8 + 12 + 16 = 36 bytes**
**Cost: 4 bytes MORE when callbacks are used**
## Code Changes Required
### 1. Update publish_state() - Hot path!
```cpp
// Current
void Sensor::internal_send_state_to_frontend(float state) {
this->callback_.call(state); // Always valid
#ifdef USE_CONTROLLER_REGISTRY
ControllerRegistry::notify_sensor_update(this);
#endif
}
// Lazy - adds nullptr check in hot path
void Sensor::internal_send_state_to_frontend(float state) {
if (this->callback_) { // ← NEW: nullptr check
this->callback_->call(state);
}
#ifdef USE_CONTROLLER_REGISTRY
ControllerRegistry::notify_sensor_update(this);
#endif
}
```
### 2. Update add_on_state_callback()
```cpp
// Current
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
this->callback_.add(std::move(callback));
}
// Lazy - lazy allocate on first use
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
if (!this->callback_) {
this->callback_ = std::make_unique<CallbackManager<void(float)>>();
}
this->callback_->add(std::move(callback));
}
```
### 3. Apply to ALL entity types
Need to update:
- Sensor, BinarySensor, TextSensor
- Climate, Fan, Light, Cover
- Switch, Lock, Valve
- Number, Select, Text, Button
- AlarmControlPanel, MediaPlayer
- etc.
## Performance Impact
**Hot path (publish_state):** Adds one nullptr check per state update
- Branch predictor should handle this well (mostly predictable per entity)
- Cost: 1-2 CPU cycles (likely free with branch prediction)
**Cold path (add callback):** Extra allocation + initialization
- Only happens during setup(), not during loop()
- Negligible impact
## Who Benefits?
### Entities WITHOUT callbacks (saves 8 bytes each):
✅ Sensors with **only** API/WebServer (no MQTT, no automations, no copy, no derivatives)
✅ Switches with **only** API/WebServer
✅ Binary sensors with **only** API/WebServer
✅ Covers, Fans, Lights, etc. with **only** API/WebServer
### Entities WITH callbacks (costs 4 bytes each):
❌ Any entity with MQTT enabled
❌ Any entity with automations (`on_value:`, `on_state:`)
❌ Copy components
❌ Derivative sensors (total_daily_energy, integration, etc.)
❌ Climate/covers with feedback sensors
## Realistic Scenario Analysis
### Scenario 1: Simple API-only device (10 sensors, no MQTT)
**Current:** 10 × 16 = 160 bytes
**Lazy:** 10 × 8 = 80 bytes
**Savings: 80 bytes**
### Scenario 2: MQTT-enabled device (10 sensors with MQTT)
**Current:** 10 × 32 = 320 bytes
**Lazy:** 10 × 36 = 360 bytes
**Cost: 40 bytes**
### Scenario 3: Mixed device (5 API-only, 5 with MQTT)
**Current:** (5 × 16) + (5 × 32) = 80 + 160 = 240 bytes
**Lazy:** (5 × 8) + (5 × 36) = 40 + 180 = 220 bytes
**Savings: 20 bytes**
### Scenario 4: Heavy automation device (10 sensors, MQTT + automation on each)
**Current:** 10 × (32 + 16) = 480 bytes (2 callbacks each)
**Lazy:** 10 × (36 + 16) = 520 bytes
**Cost: 40 bytes**
## Recommendation
### Pros:
✅ Saves 8 bytes per entity without callbacks
✅ Common case: many devices use only API/WebServer after Controller Registry
✅ Minimal code complexity (just nullptr checks)
✅ Negligible performance impact (predictable branch)
✅ Follows existing pattern (raw_callback_ is already lazy)
### Cons:
❌ Costs 4 extra bytes when callbacks ARE used (extra heap allocation)
❌ Adds nullptr check to hot path (publish_state called frequently)
❌ Requires changes to ALL entity base classes (~15+ files)
❌ Users with MQTT enabled pay the 4-byte cost
### Decision Matrix:
**Adopt if:** Most users have API-only devices (no MQTT)
**Skip if:** Most users enable MQTT or use many automations
### Data Needed:
- What % of ESPHome devices use MQTT?
- What % of entities have automations?
- Average entity count per device?
### My Recommendation: **WORTH CONSIDERING**
The savings for API-only devices are real (8 bytes per entity), and with Controller Registry, more devices are API-only. The 4-byte cost for MQTT users is small compared to MQTT's overall overhead (~60+ bytes of config per entity).
**Suggested approach:**
1. Start with Sensor (most common entity type)
2. Measure real-world impact
3. Expand to other entity types if beneficial
**Code pattern:**
```cpp
// Helper macro to reduce boilerplate
#define LAZY_CALLBACK_CALL(callback_ptr, ...) \
do { if (callback_ptr) { callback_ptr->call(__VA_ARGS__); } } while(0)
void Sensor::internal_send_state_to_frontend(float state) {
LAZY_CALLBACK_CALL(this->callback_, state);
#ifdef USE_CONTROLLER_REGISTRY
ControllerRegistry::notify_sensor_update(this);
#endif
}
```

View File

@@ -0,0 +1,286 @@
# Partitioned Callback Vector - Final Proposal
## Design
Use a **single vector** partitioned into filtered and raw sections, managed with **swap** to maintain O(1) insertion:
```cpp
// Layout: [filtered_0, ..., filtered_n-1, raw_0, ..., raw_m-1]
// ^ ^
// 0 filtered_count_
```
## Implementation
### Header (sensor.h)
```cpp
class Sensor : public EntityBase, /* ... */ {
public:
void add_on_state_callback(std::function<void(float)> &&callback);
void add_on_raw_state_callback(std::function<void(float)> &&callback);
void internal_send_state_to_frontend(float state);
void publish_state(float state);
protected:
struct Callbacks {
std::vector<std::function<void(float)>> callbacks_; // 12 bytes
uint8_t filtered_count_{0}; // 1 byte (+ 3 padding)
// Total: 16 bytes on ESP32
void add_filtered(std::function<void(float)> &&fn) {
callbacks_.push_back(std::move(fn));
if (filtered_count_ < callbacks_.size() - 1) {
// Swap new callback into filtered section
std::swap(callbacks_[filtered_count_], callbacks_[callbacks_.size() - 1]);
}
filtered_count_++;
}
void add_raw(std::function<void(float)> &&fn) {
callbacks_.push_back(std::move(fn));
}
void call_filtered(float value) {
for (size_t i = 0; i < filtered_count_; i++) {
callbacks_[i](value);
}
}
void call_raw(float value) {
for (size_t i = filtered_count_; i < callbacks_.size(); i++) {
callbacks_[i](value);
}
}
};
std::unique_ptr<Callbacks> callbacks_; // 4 bytes, lazy allocated
};
```
### Implementation (sensor.cpp)
```cpp
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<Callbacks>();
}
this->callbacks_->add_filtered(std::move(callback));
}
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<Callbacks>();
}
this->callbacks_->add_raw(std::move(callback));
}
void Sensor::publish_state(float state) {
this->raw_state = state;
// Call raw callbacks (before filters)
if (this->callbacks_) {
this->callbacks_->call_raw(state);
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
// ... filter logic ...
}
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
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)
if (this->callbacks_) {
this->callbacks_->call_filtered(state);
}
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);
#endif
}
```
## Memory Comparison (ESP32 32-bit)
### Current Implementation
```cpp
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
CallbackManager<void(float)> callback_; // 12 bytes
```
| Scenario | Current | Partitioned | Savings |
|----------|---------|-------------|---------|
| No callbacks | 16 bytes | 4 bytes | **12 bytes ✅** |
| 1 filtered (MQTT) | 32 bytes | 33 bytes | -1 byte ⚠️ |
| 2 filtered | 48 bytes | 49 bytes | -1 byte ⚠️ |
| 1 raw + 1 filtered | 60 bytes | 49 bytes | **11 bytes ✅** |
| 2 raw + 2 filtered | 92 bytes | 65 bytes | **27 bytes ✅** |
Wait, let me recalculate this more carefully...
### Corrected Memory Analysis
**Current:**
- No callbacks: 16 bytes (4 ptr + 12 vec)
- 1 filtered: 16 + 16 (fn) = 32 bytes
- 2 filtered: 16 + 32 (2 fns) = 48 bytes
- 1 raw + 1 filtered: 16 + 12 (raw vec) + 16 (raw fn) + 16 (filtered fn) = 60 bytes
**Partitioned:**
- No callbacks: 4 bytes (nullptr)
- 1 filtered: 4 (ptr) + 16 (Callbacks struct) + 16 (fn) = 36 bytes
- 2 filtered: 4 + 16 + 32 (2 fns) = 52 bytes
- 1 raw + 1 filtered: 4 + 16 + 32 (2 fns) = 52 bytes
Hmm, the struct is 16 bytes (12 vec + 1 count + 3 padding), so:
Actually on ESP32:
- std::vector = 12 bytes (3 pointers)
- uint8_t = 1 byte
- padding = 3 bytes (to align to 4)
- Total struct: 16 bytes
But when heap allocated, the struct size is what matters for memory consumption. Let me revise:
**Partitioned (heap-allocated Callbacks struct):**
- Callbacks struct on heap: 12 (vector struct) + 1 (count) + 3 (padding) = 16 bytes
- Vector data on heap: N × 16 bytes for N callbacks
So:
- No callbacks: 4 bytes (nullptr) ✅ SAVES 12
- 1 filtered: 4 (ptr) + 16 (struct) + 16 (fn) = 36 bytes ⚠️ COSTS 4
- 2 filtered: 4 + 16 + 32 = 52 bytes ⚠️ COSTS 4
- 1 raw + 1 filtered: 4 + 16 + 32 = 52 bytes ✅ SAVES 8
Actually wait - in the current implementation, when we have raw + filtered, we have:
- 16 bytes base
- 12 bytes for raw CallbackManager (heap allocated)
- 16 bytes for raw std::function
- 16 bytes for filtered std::function
= 60 bytes total
With partitioned:
- 4 bytes (ptr)
- 16 bytes (Callbacks struct on heap)
- 16 bytes (raw fn)
- 16 bytes (filtered fn)
= 52 bytes total
SAVES 8 bytes ✅
Let me make a cleaner table:
| Scenario | Current | Partitioned | Savings |
|----------|---------|-------------|---------|
| No callbacks | 16 | 4 | **+12 ✅** |
| 1 filtered only | 32 | 36 | **-4 ⚠️** |
| 1 raw only | 44 | 36 | **+8 ✅** |
| 1 raw + 1 filtered | 60 | 52 | **+8 ✅** |
| 2 filtered only | 48 | 52 | **-4 ⚠️** |
| 10 API-only sensors | 160 | 40 | **+120 ✅** |
| 10 MQTT sensors | 320 | 360 | **-40 ⚠️** |
## Performance Characteristics
### Time Complexity
- `add_filtered()`: **O(1)** - append + swap
- `add_raw()`: **O(1)** - append
- `call_filtered()`: **O(n)** - iterate filtered section
- `call_raw()`: **O(m)** - iterate raw section
### Hot Path (publish_state)
**Before:**
```cpp
if (this->callback_) {
this->callback_.call(state); // Direct call
}
```
**After:**
```cpp
if (this->callbacks_) {
for (size_t i = 0; i < callbacks_->filtered_count_; i++) {
callbacks_->callbacks_[i](state);
}
}
```
**Performance impact:**
- Adds nullptr check (already present for raw_callback_)
- Loop is tight, no branching inside
- Better cache locality than separate vectors
- Negligible impact for 0-2 callbacks (typical case)
## Advantages
1.**Best memory savings**: 12 bytes per entity without callbacks
2.**O(1) insertion**: Both filtered and raw use append (+ optional swap)
3.**No branching**: Hot path has no `if (type == FILTERED)` checks
4.**Cache friendly**: Callbacks stored contiguously
5.**Simple**: One container instead of two
6.**Minimal overhead**: Only 1 byte (+ padding) for partition count
## Disadvantages
1. ⚠️ **Costs 4 bytes** for entities with callbacks (vs current)
- But saves 12 bytes for entities WITHOUT callbacks (more common after Controller Registry)
2. ⚠️ **Swap on filtered insertion**
- Only during setup(), not runtime
- O(1) operation
- Negligible impact
3. ⚠️ **Order not preserved** within each section
- Not a problem - callback order doesn't matter
- MQTT and automation callbacks are independent
## Recommendation
**IMPLEMENT THIS!**
The partitioned vector with swap is the optimal design because:
- After Controller Registry, most entities have 0 callbacks (12-byte savings)
- Entities with callbacks pay only 4 extra bytes
- O(1) operations, no performance degradation
- Cleaner, simpler code
**Migration strategy:**
1. Implement for Sensor first
2. Measure real-world impact on flash/RAM
3. Apply to BinarySensor, TextSensor
4. Expand to other entity types (Climate, Fan, etc.)
## Code Reusability
The `Callbacks` struct can be templated for reuse across entity types:
```cpp
template<typename... Args>
struct PartitionedCallbacks {
std::vector<std::function<void(Args...)>> callbacks_;
uint8_t filtered_count_{0};
void add_filtered(std::function<void(Args...)> &&fn) { /* ... */ }
void add_raw(std::function<void(Args...)> &&fn) { /* ... */ }
void call_filtered(Args... args) { /* ... */ }
void call_raw(Args... args) { /* ... */ }
};
// Usage in different entity types:
class Sensor {
std::unique_ptr<PartitionedCallbacks<float>> callbacks_;
};
class TextSensor {
std::unique_ptr<PartitionedCallbacks<std::string>> callbacks_;
};
class Climate {
std::unique_ptr<PartitionedCallbacks<Climate&>> callbacks_;
};
```

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
aioesphomeapi==42.7.0
aioesphomeapi==42.6.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.16 # dashboard_import

View File

@@ -86,7 +86,6 @@ ISOLATED_COMPONENTS = {
"modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus",
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
"packages": "cannot merge packages",
"tinyusb": "Conflicts with usb_host component - cannot be used together",
}

View File

@@ -178,14 +178,6 @@ api:
- logger.log: "Skipped loops"
- logger.log: "After combined test"
event:
- platform: template
name: Test Event
id: test_event
event_types:
- single_click
- double_click
globals:
- id: api_continuation_test_counter
type: int

View File

@@ -52,25 +52,3 @@ sensor:
name: "BLE Sensor without Lambda"
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678"
number:
- platform: template
name: "Test Number"
id: test_number
optimistic: true
min_value: 0
max_value: 255
step: 1
button:
# Test ble_write with lambda that references a component (function pointer)
- platform: template
name: "BLE Write Lambda Test"
on_press:
- ble_client.ble_write:
id: test_blec
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678"
value: !lambda |-
uint8_t val = (uint8_t)id(test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};

View File

@@ -37,15 +37,6 @@ canbus:
break;
}
number:
- platform: template
name: "Test Number"
id: test_number
optimistic: true
min_value: 0
max_value: 255
step: 1
button:
- platform: template
name: Canbus Actions
@@ -53,7 +44,3 @@ button:
- canbus.send: "abc"
- canbus.send: [0, 1, 2]
- canbus.send: !lambda return {0, 1, 2};
# Test canbus.send with lambda that references a component (function pointer)
- canbus.send: !lambda |-
uint8_t val = (uint8_t)id(test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};

View File

@@ -16,6 +16,5 @@ display:
touchscreen:
- platform: chsc6x
i2c_id: i2c_bus
display: ili9xxx_display
interrupt_pin: 22

View File

@@ -1,41 +0,0 @@
esphome:
on_boot:
then:
- hlk_fm22x.enroll:
name: "Test"
direction: 1
- hlk_fm22x.delete_all:
hlk_fm22x:
on_face_scan_matched:
- logger.log: test_hlk_22x_face_scan_matched
on_face_scan_unmatched:
- logger.log: test_hlk_22x_face_scan_unmatched
on_face_scan_invalid:
- logger.log: test_hlk_22x_face_scan_invalid
on_face_info:
- logger.log: test_hlk_22x_face_info
on_enrollment_done:
- logger.log: test_hlk_22x_enrollment_done
on_enrollment_failed:
- logger.log: test_hlk_22x_enrollment_failed
sensor:
- platform: hlk_fm22x
face_count:
name: "Face Count"
last_face_id:
name: "Last Face ID"
status:
name: "Face Status"
binary_sensor:
- platform: hlk_fm22x
name: "Face Enrolling"
text_sensor:
- platform: hlk_fm22x
version:
name: "HLK Version"
last_face_name:
name: "Last Face Name"

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
<<: !include common.yaml

View File

@@ -1,11 +1,3 @@
number:
- platform: template
id: test_number
optimistic: true
min_value: 0
max_value: 255
step: 1
button:
- platform: template
name: Beo4 audio mute
@@ -136,16 +128,10 @@ button:
address: 0x00
command: 0x0B
- platform: template
name: RC5 Raw static
name: RC5 Raw
on_press:
remote_transmitter.transmit_raw:
code: [1000, -1000]
- platform: template
name: RC5 Raw lambda
on_press:
remote_transmitter.transmit_raw:
code: !lambda |-
return {(int32_t)id(test_number).state * 100, -1000};
- platform: template
name: AEHA
id: eaha_hitachi_climate_power_on
@@ -231,23 +217,6 @@ button:
command: 0xEC
rc_code_1: 0x0D
rc_code_2: 0x0D
- platform: template
name: ABBWelcome static
on_press:
remote_transmitter.transmit_abbwelcome:
source_address: 0x1234
destination_address: 0x5678
message_type: 0x01
data: [0x10, 0x20, 0x30]
- platform: template
name: ABBWelcome lambda
on_press:
remote_transmitter.transmit_abbwelcome:
source_address: 0x1234
destination_address: 0x5678
message_type: 0x01
data: !lambda |-
return {(uint8_t)id(test_number).state, 0x20, 0x30};
- platform: template
name: Digital Write
on_press:

View File

@@ -1,52 +1,18 @@
number:
- platform: template
name: "Speaker Number"
id: my_number
optimistic: true
min_value: 0
max_value: 100
step: 1
esphome:
on_boot:
then:
- speaker.mute_on:
id: speaker_id
- speaker.mute_off:
id: speaker_id
- if:
condition:
speaker.is_stopped:
id: speaker_id
condition: speaker.is_stopped
then:
- speaker.play:
id: speaker_id
data: [0, 1, 2, 3]
- speaker.volume_set:
id: speaker_id
volume: 0.9
- speaker.play: [0, 1, 2, 3]
- speaker.volume_set: 0.9
- if:
condition:
speaker.is_playing:
id: speaker_id
condition: speaker.is_playing
then:
- speaker.finish:
id: speaker_id
- speaker.stop:
id: speaker_id
button:
- platform: template
name: "Speaker Button"
on_press:
then:
- speaker.play:
id: speaker_id
data: [0x10, 0x20, 0x30, 0x40]
- speaker.play:
id: speaker_id
data: !lambda |-
return {0x01, 0x02, (uint8_t)id(my_number).state};
i2s_audio:
i2s_lrclk_pin: ${i2s_bclk_pin}

View File

@@ -26,15 +26,6 @@ sx126x:
- lambda: |-
ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str());
number:
- platform: template
name: "SX126x Number"
id: my_number
optimistic: true
min_value: 0
max_value: 100
step: 1
button:
- platform: template
name: "SX126x Button"
@@ -46,5 +37,3 @@ button:
- sx126x.set_mode_rx
- sx126x.send_packet:
data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
- sx126x.send_packet: !lambda |-
return {0x01, 0x02, (uint8_t)id(my_number).state};

View File

@@ -26,15 +26,6 @@ sx127x:
- sx127x.send_packet:
data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
number:
- platform: template
name: "SX127x Number"
id: my_number
optimistic: true
min_value: 0
max_value: 100
step: 1
button:
- platform: template
name: "SX127x Button"
@@ -47,5 +38,3 @@ button:
- sx127x.set_mode_rx
- sx127x.send_packet:
data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
- sx127x.send_packet: !lambda |-
return {0x01, 0x02, (uint8_t)id(my_number).state};

View File

@@ -1,3 +1,11 @@
remote_transmitter:
pin: ${tx_pin}
carrier_duty_percent: 50%
remote_receiver:
id: rcvr
pin: ${rx_pin}
climate:
- platform: toshiba
name: "RAS-2819T Climate"

View File

@@ -1,5 +1,5 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-ard.yaml
substitutions:
tx_pin: GPIO5
rx_pin: GPIO4
<<: !include common_ras2819t.yaml

View File

@@ -1,5 +1,5 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-ard.yaml
substitutions:
tx_pin: GPIO5
rx_pin: GPIO4
<<: !include common_ras2819t.yaml

View File

@@ -1,5 +1,5 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
substitutions:
tx_pin: GPIO5
rx_pin: GPIO4
<<: !include common_ras2819t.yaml

View File

@@ -1,5 +1,5 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
substitutions:
tx_pin: GPIO5
rx_pin: GPIO4
<<: !include common_ras2819t.yaml

View File

@@ -3,8 +3,6 @@ esphome:
then:
- uart.write: 'Hello World'
- uart.write: [0x00, 0x20, 0x42]
- uart.write: !lambda |-
return {0xAA, 0xBB, 0xCC};
uart:
- id: uart_uart
@@ -48,15 +46,6 @@ switch:
turn_on: "TURN_ON"
turn_off: "TURN_OFF"
number:
- platform: template
name: "Test Number"
id: test_number
optimistic: true
min_value: 0
max_value: 100
step: 1
button:
# Test uart button with array data
- platform: uart
@@ -68,10 +57,3 @@ button:
name: "UART Button String"
uart_id: uart_uart
data: "BUTTON_PRESS"
# Test uart button with lambda (function pointer)
- platform: template
name: "UART Lambda Test"
on_press:
- uart.write: !lambda |-
std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n";
return std::vector<uint8_t>(cmd.begin(), cmd.end());

View File

@@ -17,22 +17,3 @@ udp:
id: my_udp
data: !lambda |-
return std::vector<uint8_t>{1,3,4,5,6};
number:
- platform: template
name: "UDP Number"
id: my_number
optimistic: true
min_value: 0
max_value: 100
step: 1
button:
- platform: template
name: "UDP Button"
on_press:
then:
- udp.write:
data: [0x01, 0x02, 0x03]
- udp.write: !lambda |-
return {0x10, 0x20, (uint8_t)id(my_number).state};

View File

@@ -1,12 +0,0 @@
# Common remote_receiver configuration for ESP32 Arduino tests
# Provides a shared remote receiver that all components can use
# Components will auto-use this receiver if they don't specify receiver_id
substitutions:
remote_receiver_pin: GPIO32
remote_receiver:
- id: rcvr
pin: ${remote_receiver_pin}
dump: all
tolerance: 25%

View File

@@ -1,12 +0,0 @@
# Common remote_receiver configuration for ESP32-C3 Arduino tests
# Provides a shared remote receiver that all components can use
# Components will auto-use this receiver if they don't specify receiver_id
substitutions:
remote_receiver_pin: GPIO10
remote_receiver:
- id: rcvr
pin: ${remote_receiver_pin}
dump: all
tolerance: 25%

View File

@@ -670,51 +670,3 @@ class TestEsphomeCore:
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == Path(expected_default)
def test_web_port__none(self, target):
"""Test web_port returns None when web_server is not configured."""
target.config = {}
assert target.web_port is None
def test_web_port__explicit_web_server_default_port(self, target):
"""Test web_port returns 80 when web_server is explicitly configured without port."""
target.config = {const.CONF_WEB_SERVER: {}}
assert target.web_port == 80
def test_web_port__explicit_web_server_custom_port(self, target):
"""Test web_port returns custom port when web_server is configured with port."""
target.config = {const.CONF_WEB_SERVER: {const.CONF_PORT: 8080}}
assert target.web_port == 8080
def test_web_port__ota_web_server_platform_only(self, target):
"""
Test web_port returns None when ota.web_server platform is explicitly configured.
This is a critical test for Dashboard Issue #766:
https://github.com/esphome/dashboard/issues/766
When ota: platform: web_server is explicitly configured (or auto-loaded by captive_portal):
- "web_server" appears in loaded_integrations (platform name added to integrations)
- "ota/web_server" appears in loaded_platforms
- But CONF_WEB_SERVER is NOT in config (only the platform is loaded, not the component)
- web_port MUST return None (no web UI available)
- Dashboard should NOT show VISIT button
This test ensures web_port only checks CONF_WEB_SERVER in config, not loaded_integrations.
"""
# Simulate config with ota.web_server platform but no web_server component
# This happens when:
# 1. User explicitly configures: ota: - platform: web_server
# 2. OR captive_portal auto-loads ota.web_server
target.config = {
const.CONF_OTA: [
{
"platform": "web_server",
# OTA web_server platform config would be here
}
],
# Note: CONF_WEB_SERVER is NOT in config - only the OTA platform
}
# Even though "web_server" is in loaded_integrations due to the platform,
# web_port must return None because the full web_server component is not configured
assert target.web_port is None

View File

@@ -0,0 +1,208 @@
# Unified Callback Storage - Proposal
## Current Implementation (ESP32 32-bit)
```cpp
class Sensor {
protected:
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
CallbackManager<void(float)> callback_; // 12 bytes
};
```
**Memory costs:**
- No callbacks: **16 bytes**
- 1 filtered callback (MQTT): **32 bytes** (16 + 16 function)
- 1 raw + 1 filtered: **60 bytes** (16 + 12 + 16 + 16)
## Proposed: Single Vector with Type Flag
```cpp
class Sensor {
protected:
enum CallbackType : uint8_t { RAW, FILTERED };
struct CallbackEntry {
CallbackType type; // 1 byte (+ 3 bytes padding)
std::function<void(float)> fn; // 16 bytes
// Total: 20 bytes per callback
};
std::unique_ptr<std::vector<CallbackEntry>> callbacks_; // 4 bytes, lazy allocated
};
```
**Memory costs:**
- No callbacks: **4 bytes****SAVES 12 BYTES**
- 1 filtered callback (MQTT): **36 bytes** (4 + 12 vector + 20 entry) ⚠️ **COSTS 4 BYTES**
- 1 raw + 1 filtered: **56 bytes** (4 + 12 + 20 + 20) ✅ **SAVES 4 BYTES**
## Alternative: Two Vectors in One Struct
```cpp
struct SensorCallbacks {
std::vector<std::function<void(float)>> raw_callbacks; // 12 bytes
std::vector<std::function<void(float)>> filtered_callbacks; // 12 bytes
// Total: 24 bytes overhead
};
class Sensor {
protected:
std::unique_ptr<SensorCallbacks> callbacks_; // 4 bytes, lazy allocated
};
```
**Memory costs:**
- No callbacks: **4 bytes****SAVES 12 BYTES**
- 1 filtered callback: **44 bytes** (4 + 24 struct + 16 function) ❌ **COSTS 12 BYTES**
- 1 raw + 1 filtered: **60 bytes** (4 + 24 + 16 + 16) ✅ **SAME**
## Recommendation: Single Vector with Type
The single vector approach is better because:
1. ✅ Same 12-byte savings when no callbacks (most common after Controller Registry)
2. ✅ Only 4-byte cost when callbacks ARE used (vs 12-byte cost for two-vectors)
3. ✅ Simpler: one vector to manage instead of two
4. ✅ Better cache locality: callbacks stored together
## Implementation
### Header
```cpp
class Sensor : public EntityBase, /* ... */ {
public:
void add_on_state_callback(std::function<void(float)> &&callback);
void add_on_raw_state_callback(std::function<void(float)> &&callback);
void internal_send_state_to_frontend(float state);
protected:
enum CallbackType : uint8_t { RAW, FILTERED };
struct CallbackEntry {
CallbackType type;
std::function<void(float)> fn;
};
std::unique_ptr<std::vector<CallbackEntry>> callbacks_;
// Helper to allocate on first use
void ensure_callbacks_() {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<std::vector<CallbackEntry>>();
}
}
};
```
### Implementation
```cpp
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
this->ensure_callbacks_();
this->callbacks_->push_back({FILTERED, std::move(callback)});
}
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
this->ensure_callbacks_();
this->callbacks_->push_back({RAW, std::move(callback)});
}
void Sensor::internal_send_state_to_frontend(float state) {
// Call filtered callbacks
if (this->callbacks_) {
for (auto &entry : *this->callbacks_) {
if (entry.type == FILTERED) {
entry.fn(state);
}
}
}
#ifdef USE_CONTROLLER_REGISTRY
ControllerRegistry::notify_sensor_update(this);
#endif
}
void Sensor::publish_state(float state) {
// Call raw callbacks first
if (this->callbacks_) {
for (auto &entry : *this->callbacks_) {
if (entry.type == RAW) {
entry.fn(state);
}
}
}
// Then filters, which eventually call internal_send_state_to_frontend
// ... existing filter logic ...
}
```
## Performance Impact
**Hot path (publish_state):**
- Before: Direct vector iteration
- After: Vector iteration with type check (`if (entry.type == FILTERED)`)
- Cost: ~1 CPU cycle per callback (trivial branch)
- Most entities have 0-2 callbacks, so negligible
**Cold path (add_on_state_callback):**
- Extra check + possible allocation
- Only happens during setup()
- Negligible impact
## Migration Path
1. **Start with Sensor** (most common entity type)
2. Measure real-world impact
3. Apply to other entities: BinarySensor, TextSensor, Climate, Fan, etc.
## Real-World Scenarios
### Scenario 1: API-only device (10 sensors, no MQTT)
**Before:** 10 × 16 = 160 bytes
**After:** 10 × 4 = 40 bytes
**Saves: 120 bytes**
### Scenario 2: MQTT-enabled (10 sensors)
**Before:** 10 × 32 = 320 bytes
**After:** 10 × 36 = 360 bytes
**Costs: 40 bytes** ⚠️
### Scenario 3: Mixed (5 API-only + 5 MQTT)
**Before:** (5 × 16) + (5 × 32) = 240 bytes
**After:** (5 × 4) + (5 × 36) = 200 bytes
**Saves: 40 bytes**
### Scenario 4: Heavy automation (10 sensors, MQTT + automation each)
**Before:** 10 × (16 + 16 + 16) = 480 bytes (base + MQTT fn + automation fn)
**After:** 10 × (4 + 12 + 20 + 20) = 560 bytes (ptr + vector + 2 entries)
**Costs: 80 bytes** ⚠️
Wait, let me recalculate this more carefully...
Actually with 2 callbacks:
**Before:** 16 (base) + 16 (MQTT fn) + 16 (automation fn) = 48 bytes per sensor
**After:** 4 (ptr) + 12 (vector) + 20 (MQTT entry) + 20 (automation entry) = 56 bytes per sensor
**Costs: 8 bytes per sensor with 2+ callbacks** ⚠️
### Revised Decision Matrix
**Wins:**
- Entities with 0 callbacks: Save 12 bytes (most common after Controller Registry)
- Entities with 2+ different types (raw + filtered): Save 4 bytes
**Loses:**
- Entities with 1 callback: Cost 4 bytes
- Entities with 2+ callbacks of same type: Cost 8 bytes
**Net benefit:** Positive if >75% of entities have 0 callbacks
## Conclusion
**RECOMMENDED** for implementation because:
1. ✅ After Controller Registry, most entities are API-only (0 callbacks)
2. ✅ 12-byte savings per entity adds up quickly (120 bytes for 10 sensors)
3. ✅ 4-8 byte cost for MQTT users is acceptable (MQTT already has ~60+ bytes overhead)
4. ✅ Simpler code: one container instead of two
5. ✅ Negligible performance impact (predictable branch on hot path)
**Start with Sensor, measure, then expand to other entity types.**

View File

@@ -0,0 +1,204 @@
# Plan: Remove EventEmitter and Replace with Simple Callbacks
## Overview
This plan describes the architectural change from using the EventEmitter component to direct callback storage in ESP32 BLE server classes. This simplifies the codebase by removing template metaprogramming and code generation complexity.
## Motivation
The previous EventEmitter-based approach had several issues:
1. **Memory overhead**: Each EventEmitter instance stored a `std::vector` of listeners, even when only 0-1 listeners were needed
2. **Complexity**: Required template metaprogramming with per-UUID specialized classes
3. **Code generation**: Required Python code generation to create specialized classes
4. **Allocation tracking**: Required complex API to pre-allocate listener slots
## New Approach
Replace EventEmitter with direct callback storage using `std::function`:
- Each BLE object (characteristic, descriptor, server) stores callbacks directly as member variables
- Only one callback per event type (sufficient for ESPHome's automation framework)
- No code generation needed
- No allocation tracking needed
- Simpler, more maintainable code
## Implementation Changes
### 1. BLECharacteristic (ble_characteristic.h/cpp)
**Removed:**
- `EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>` inheritance
- `EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>` inheritance
- `BLECharacteristicEvt` namespace with event enums
- Virtual `emit_on_write_()` and `emit_on_read_()` methods
**Added:**
- Direct callback registration methods:
```cpp
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback);
void on_read(std::function<void(uint16_t)> &&callback);
```
- Callback member variables:
```cpp
std::function<void(std::span<const uint8_t>, uint16_t)> on_write_callback_{nullptr};
std::function<void(uint16_t)> on_read_callback_{nullptr};
```
**Implementation:**
- In `gatts_event_handler()`, replaced `emit_on_write_()` with direct callback invocation:
```cpp
if (this->on_write_callback_) {
this->on_write_callback_(this->value_, param->write.conn_id);
}
```
### 2. BLEDescriptor (ble_descriptor.h/cpp)
**Removed:**
- `EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t>` inheritance
- `BLEDescriptorEvt` namespace with event enums
- Virtual `emit_on_write_()` method
**Added:**
- Direct callback registration:
```cpp
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback);
```
- Callback member variable:
```cpp
std::function<void(std::span<const uint8_t>, uint16_t)> on_write_callback_{nullptr};
```
### 3. BLEServer (ble_server.h/cpp)
**Removed:**
- `EventEmitter<BLEServerEvt::EmptyEvt, uint16_t>` inheritance
- `BLEServerEvt` namespace with event enums
- Virtual `emit_on_connect_()` and `emit_on_disconnect_()` methods
**Added:**
- Direct callback registration:
```cpp
void on_connect(std::function<void(uint16_t)> &&callback);
void on_disconnect(std::function<void(uint16_t)> &&callback);
```
- Callback member variables:
```cpp
std::function<void(uint16_t)> on_connect_callback_{nullptr};
std::function<void(uint16_t)> on_disconnect_callback_{nullptr};
```
### 4. BLE Server Automations (ble_server_automations.h/cpp)
**Removed:**
- EventEmitter listener ID tracking (`EventEmitterListenerID`)
- `INVALID_LISTENER_ID` constant
- Complex listener ID management in `BLECharacteristicSetValueActionManager`
**Simplified:**
- `BLECharacteristicSetValueActionManager` now just tracks presence of listener (not ID)
- Uses simple `has_listener()` check instead of ID comparison
- Callback registration uses direct `on_write()` and `on_read()` methods
### 5. Python Code Generation (__init__.py)
**Removed:**
- Entire allocation API (functions to pre-allocate listener slots)
- All code generation for specialized per-UUID classes
- Allocation tracking data structures
- EventEmitter from `AUTO_LOAD`
**Result:**
- ~700 lines of complex code generation removed
- Simpler `to_code()` functions
- No allocation calls needed in component configurations
### 6. EventEmitter Component
**Removed:**
- Entire `esphome/components/event_emitter/` directory deleted
- No longer needed since callbacks are stored directly
### 7. Components Using BLE Server (esp32_improv)
**Changes:**
- Removed EventEmitter allocation calls
- Uses direct callback registration via existing interface
- No other changes needed (uses virtual methods that call callbacks)
## Benefits
1. **Reduced memory usage**: No `std::vector` overhead for each event type
2. **Simpler codebase**: ~700 lines of complex code removed
3. **Easier to maintain**: No template metaprogramming or code generation
4. **Faster compilation**: No specialized classes to generate
5. **Clearer intent**: Direct callbacks are easier to understand than EventEmitter indirection
## Technical Details
### Callback Signature Changes
Using `std::span<const uint8_t>` for zero-copy data passing:
- **Before**: `std::vector<uint8_t>` (requires copy)
- **After**: `std::span<const uint8_t>` (zero-copy view)
### Callback Storage
Using `std::function` with nullptr initialization:
```cpp
std::function<void(...)> callback_{nullptr};
```
Checked before invocation:
```cpp
if (this->callback_) {
this->callback_(...);
}
```
### Callback Registration
Callbacks moved into storage to avoid copies:
```cpp
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ = std::move(callback);
}
```
### Lambda Captures
Automation framework registers lambdas with captures:
```cpp
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
```
## Testing
After implementation:
1. Test ESP32 BLE server compilation with various configurations
2. Test esp32_improv component (uses BLE server)
3. Verify memory usage improvements
4. Verify functionality with BLE automations
## Files Modified
- `esphome/components/esp32_ble_server/ble_characteristic.h`
- `esphome/components/esp32_ble_server/ble_characteristic.cpp`
- `esphome/components/esp32_ble_server/ble_descriptor.h`
- `esphome/components/esp32_ble_server/ble_descriptor.cpp`
- `esphome/components/esp32_ble_server/ble_server.h`
- `esphome/components/esp32_ble_server/ble_server.cpp`
- `esphome/components/esp32_ble_server/ble_server_automations.h`
- `esphome/components/esp32_ble_server/ble_server_automations.cpp`
- `esphome/components/esp32_ble_server/__init__.py`
- `esphome/components/esp32_improv/__init__.py`
## Files Deleted
- `esphome/components/event_emitter/` (entire directory)
## Summary
This change dramatically simplifies the ESP32 BLE server implementation while reducing memory usage and improving maintainability. The direct callback approach is more idiomatic C++ and easier to understand than the EventEmitter abstraction.

90
usb_callback_contexts.md Normal file
View File

@@ -0,0 +1,90 @@
# USB Host Component - Callback Execution Contexts
## Overview
After the refactoring to use a dedicated USB task, all USB callbacks now execute in the USB task context, NOT in the main loop. This prevents race conditions and data corruption.
## USB Task Architecture
```cpp
// USB Task (runs on Core 1, priority 5)
void USBClient::usb_task_loop() {
while (usb_task_running_) {
// This handles ALL USB events and triggers callbacks
usb_host_client_handle_events(handle_, pdMS_TO_TICKS(10));
}
}
```
## Callback Execution Contexts
### 1. **client_event_cb** (Device Connect/Disconnect)
- **Context**: USB task
- **Triggered by**: USB device connection/disconnection events
- **What it does**: Queues events to main loop via FreeRTOS queue
- **Data flow**: USB hardware → USB task → Queue → Main loop
### 2. **control_callback** (Control Transfers)
- **Context**: USB task
- **Triggered by**: Completion of USB control transfers
- **What it does**: Queues callback execution to main loop
- **Data flow**: USB hardware → USB task → Queue → Main loop
### 3. **transfer_callback** (Bulk Transfers)
- **Context**: USB task
- **Triggered by**: Completion of bulk IN/OUT transfers
- **What it does**: Queues callback execution to main loop
- **Data flow**: USB hardware → USB task → Queue → Main loop
### 4. **USBUartComponent Input Callback** (Data Reception)
- **Context**: USB task (lambda passed to transfer_in)
- **Called from**: transfer_callback in USB task
- **What it does**:
- Copies received data to temporary buffer
- Queues data processing to main loop via defer()
- Main loop then safely pushes to ring buffer
- **Data flow**: USB hardware → USB task → Copy data → Queue → Main loop → Ring buffer
### 5. **USBUartComponent Output Callback** (Data Transmission)
- **Context**: USB task (lambda passed to transfer_out)
- **Called from**: transfer_callback in USB task
- **What it does**:
- Marks output as not started
- Queues next transfer start to main loop via defer()
- **Data flow**: Main loop reads ring buffer → Start transfer → USB task → Queue restart → Main loop
## Thread Safety Summary
### Safe Operations (Main Loop Only):
- Ring buffer push (incoming data)
- Ring buffer pop (outgoing data)
- Component state changes
- Transfer initiation
### USB Task Operations:
- USB event handling
- Transfer completion detection
- Event queueing to main loop
### Communication Mechanism:
- **FreeRTOS Queue**: For USB events and callbacks
- **defer()**: For component-specific operations
## Why This Architecture?
1. **Prevents data loss**: USB events are handled promptly even when main loop is busy
2. **Thread safety**: Ring buffer is only accessed from main loop
3. **No race conditions**: Data structures aren't shared between tasks
4. **Maintains responsiveness**: USB hardware FIFOs don't overflow
## Key Changes from Original Implementation
### Before (Problematic):
- `usb_host_client_handle_events()` called in main loop
- Callbacks executed in main loop context
- USB events dropped when main loop was busy
- Ring buffer corruption when data arrived during slow processing
### After (Fixed):
- `usb_host_client_handle_events()` runs in dedicated task
- Callbacks execute in USB task, queue work to main loop
- USB events always handled promptly
- Ring buffer only accessed from main loop (thread-safe)