mirror of
https://github.com/esphome/esphome.git
synced 2025-10-26 12:43:48 +00:00
wi
This commit is contained in:
@@ -1,200 +0,0 @@
|
|||||||
# EnumBitmask Pattern Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
`EnumBitmask<EnumType, MaxBits>` from `esphome/core/enum_bitmask.h` provides a memory-efficient replacement for `std::set<EnumType>` when storing sets of enum values.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
Use `EnumBitmask` instead of `std::set<EnumType>` when:
|
|
||||||
- Storing sets of enum values (e.g., supported modes, capabilities)
|
|
||||||
- Enum has ≤32 distinct values
|
|
||||||
- Memory efficiency is important (saves ~586 bytes per `std::set` instance)
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
- **Memory Savings**: Eliminates red-black tree overhead (~586 bytes per instance)
|
|
||||||
- **Compact Storage**: 1-4 bytes depending on enum count (uint8_t/uint16_t/uint32_t)
|
|
||||||
- **Constexpr-Compatible**: Supports compile-time initialization
|
|
||||||
- **Efficient Iteration**: Only visits set bits, not all possible enum values
|
|
||||||
- **Range-Based Loops**: `for (auto value : mask)` works seamlessly
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
1. Enum must have sequential values (or use a lookup table for mapping)
|
|
||||||
2. Maximum 32 enum values (uint32_t bitmask limitation)
|
|
||||||
3. Must provide template specializations for `enum_to_bit()` and `bit_to_enum()`
|
|
||||||
|
|
||||||
## Basic Usage Example
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Bad - red-black tree overhead (~586 bytes)
|
|
||||||
std::set<ColorMode> supported_modes;
|
|
||||||
supported_modes.insert(ColorMode::RGB);
|
|
||||||
supported_modes.insert(ColorMode::WHITE);
|
|
||||||
if (supported_modes.count(ColorMode::RGB)) { ... }
|
|
||||||
|
|
||||||
// Good - compact bitmask storage (2-4 bytes)
|
|
||||||
ColorModeMask supported_modes({ColorMode::RGB, ColorMode::WHITE});
|
|
||||||
if (supported_modes.contains(ColorMode::RGB)) { ... }
|
|
||||||
for (auto mode : supported_modes) { ... } // Iterate over set values
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Pattern
|
|
||||||
|
|
||||||
### 1. Define the Lookup Table
|
|
||||||
|
|
||||||
If enum values aren't sequential from 0, create a lookup table:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// In your component header (e.g., esphome/components/light/color_mode.h)
|
|
||||||
constexpr ColorMode COLOR_MODE_LOOKUP[10] = {
|
|
||||||
ColorMode::UNKNOWN, // bit 0
|
|
||||||
ColorMode::ON_OFF, // bit 1
|
|
||||||
ColorMode::BRIGHTNESS, // bit 2
|
|
||||||
ColorMode::WHITE, // bit 3
|
|
||||||
ColorMode::COLOR_TEMPERATURE, // bit 4
|
|
||||||
ColorMode::COLD_WARM_WHITE, // bit 5
|
|
||||||
ColorMode::RGB, // bit 6
|
|
||||||
ColorMode::RGB_WHITE, // bit 7
|
|
||||||
ColorMode::RGB_COLOR_TEMPERATURE, // bit 8
|
|
||||||
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create Type Alias
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
constexpr int COLOR_MODE_BITMASK_SIZE = 10;
|
|
||||||
using ColorModeMask = EnumBitmask<ColorMode, COLOR_MODE_BITMASK_SIZE>;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Provide Template Specializations
|
|
||||||
|
|
||||||
**IMPORTANT**: Specializations must be in the **global namespace** (C++ requirement). Place them at the end of your header file, outside your component namespace.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// At end of header, outside namespace esphome::light
|
|
||||||
// Template specializations for ColorMode must be in global namespace
|
|
||||||
//
|
|
||||||
// C++ requires template specializations to be declared in the same namespace as the
|
|
||||||
// original template. Since EnumBitmask is in the esphome namespace (not esphome::light),
|
|
||||||
// we must provide these specializations at global scope with fully-qualified names.
|
|
||||||
//
|
|
||||||
// These specializations define how ColorMode enum values map to/from bit positions.
|
|
||||||
|
|
||||||
/// Map ColorMode enum values to bit positions (0-9)
|
|
||||||
template<>
|
|
||||||
constexpr int esphome::EnumBitmask<esphome::light::ColorMode,
|
|
||||||
esphome::light::COLOR_MODE_BITMASK_SIZE>::enum_to_bit(
|
|
||||||
esphome::light::ColorMode mode) {
|
|
||||||
// Map enum value to bit position (0-9)
|
|
||||||
for (int i = 0; i < esphome::light::COLOR_MODE_BITMASK_SIZE; ++i) {
|
|
||||||
if (esphome::light::COLOR_MODE_LOOKUP[i] == mode)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return 0; // Unknown values map to bit 0 (typically reserved for UNKNOWN/NONE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map bit positions (0-9) to ColorMode enum values
|
|
||||||
template<>
|
|
||||||
inline esphome::light::ColorMode esphome::EnumBitmask<esphome::light::ColorMode,
|
|
||||||
esphome::light::COLOR_MODE_BITMASK_SIZE>::bit_to_enum(int bit) {
|
|
||||||
return (bit >= 0 && bit < esphome::light::COLOR_MODE_BITMASK_SIZE)
|
|
||||||
? esphome::light::COLOR_MODE_LOOKUP[bit]
|
|
||||||
: esphome::light::ColorMode::UNKNOWN;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling in enum_to_bit()
|
|
||||||
|
|
||||||
The implementation returns bit 0 for unknown enum values:
|
|
||||||
```cpp
|
|
||||||
return 0; // Unknown values map to bit 0
|
|
||||||
```
|
|
||||||
|
|
||||||
This means an unknown ColorMode maps to the same bit as `ColorMode::UNKNOWN`. This is acceptable because:
|
|
||||||
- Compile-time failure occurs if using invalid enum values
|
|
||||||
- `ColorMode::UNKNOWN` at bit 0 is semantically correct
|
|
||||||
- Runtime misuse is prevented by type safety
|
|
||||||
|
|
||||||
## API Compatibility with std::set
|
|
||||||
|
|
||||||
EnumBitmask provides both modern `.contains()` / `.add()` / `.remove()` methods and std::set-compatible aliases for drop-in replacement:
|
|
||||||
|
|
||||||
| Operation | std::set | EnumBitmask | Notes |
|
|
||||||
|-----------|----------|-------------|-------|
|
|
||||||
| Add value | `.insert(value)` | `.insert(value)` or `.add(value)` | Both work |
|
|
||||||
| Check membership | `.count(value)` | `.count(value)` or `.contains(value)` | Both work |
|
|
||||||
| Remove value | `.erase(value)` | `.erase(value)` or `.remove(value)` | Both work |
|
|
||||||
| Count elements | `.size()` | `.size()` | Same |
|
|
||||||
| Check empty | `.empty()` | `.empty()` | Same |
|
|
||||||
| Clear all | `.clear()` | `.clear()` | Same |
|
|
||||||
| Iterate | `for (auto v : set)` | `for (auto v : mask)` | Same |
|
|
||||||
|
|
||||||
**Drop-in replacement**: You can use either the std::set-compatible methods (`.insert()`, `.count()`, `.erase()`) or the more explicit methods (`.add()`, `.contains()`, `.remove()`).
|
|
||||||
|
|
||||||
## Complete Usage Example
|
|
||||||
|
|
||||||
See `esphome/components/light/color_mode.h` for a complete real-world implementation showing:
|
|
||||||
- Lookup table definition
|
|
||||||
- Type aliases
|
|
||||||
- Template specializations
|
|
||||||
- Helper functions using the bitmask
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Compile-Time Initialization
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Constexpr-compatible for compile-time initialization
|
|
||||||
constexpr ColorModeMask DEFAULT_MODES({ColorMode::ON_OFF, ColorMode::BRIGHTNESS});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding Multiple Values
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
ColorModeMask modes;
|
|
||||||
modes.add({ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checking and Iterating
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
if (modes.contains(ColorMode::RGB)) {
|
|
||||||
// RGB mode is supported
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto mode : modes) {
|
|
||||||
// Process each supported mode
|
|
||||||
ESP_LOGD(TAG, "Supported mode: %d", static_cast<int>(mode));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working with Raw Bitmask Values
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Get raw bitmask for bitwise operations
|
|
||||||
auto mask = modes.get_mask();
|
|
||||||
|
|
||||||
// Check if raw bitmask contains a value
|
|
||||||
if (ColorModeMask::mask_contains(mask, ColorMode::RGB)) { ... }
|
|
||||||
|
|
||||||
// Get first value from raw bitmask
|
|
||||||
auto first = ColorModeMask::first_value_from_mask(mask);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Detection of Opportunities
|
|
||||||
|
|
||||||
Look for these patterns in existing code:
|
|
||||||
- `std::set<EnumType>` with small enum sets (≤32 values)
|
|
||||||
- Components storing "supported modes" or "capabilities"
|
|
||||||
- Red-black tree code (`rb_tree`, `_Rb_tree`) in compiler output
|
|
||||||
- Flash size increases when adding enum set storage
|
|
||||||
|
|
||||||
## When NOT to Use
|
|
||||||
|
|
||||||
- Enum has >32 distinct values (bitmask limitation)
|
|
||||||
- Need to store arbitrary runtime-determined integer values (not enum values)
|
|
||||||
- Enum values are sparse or non-sequential and lookup table would be impractical
|
|
||||||
- Code readability matters more than memory savings (niche single-use components)
|
|
||||||
@@ -23,7 +23,7 @@ namespace esphome {
|
|||||||
/// Example usage:
|
/// Example usage:
|
||||||
/// using ClimateModeMask = EnumBitmask<ClimateMode, 8>;
|
/// using ClimateModeMask = EnumBitmask<ClimateMode, 8>;
|
||||||
/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
|
/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
|
||||||
/// if (modes.contains(CLIMATE_MODE_HEAT)) { ... }
|
/// if (modes.count(CLIMATE_MODE_HEAT)) { ... }
|
||||||
/// for (auto mode : modes) { ... } // Iterate over set bits
|
/// for (auto mode : modes) { ... } // Iterate over set bits
|
||||||
///
|
///
|
||||||
/// For complete usage examples with template specializations, see:
|
/// For complete usage examples with template specializations, see:
|
||||||
@@ -48,40 +48,32 @@ template<typename EnumType, int MaxBits = 16> class EnumBitmask {
|
|||||||
/// Construct from initializer list: {VALUE1, VALUE2, ...}
|
/// Construct from initializer list: {VALUE1, VALUE2, ...}
|
||||||
constexpr EnumBitmask(std::initializer_list<EnumType> values) {
|
constexpr EnumBitmask(std::initializer_list<EnumType> values) {
|
||||||
for (auto value : values) {
|
for (auto value : values) {
|
||||||
this->add(value);
|
this->insert(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a single enum value to the set
|
/// Add a single enum value to the set (std::set compatibility)
|
||||||
constexpr void add(EnumType value) { this->mask_ |= (static_cast<bitmask_t>(1) << enum_to_bit(value)); }
|
constexpr void insert(EnumType value) { this->mask_ |= (static_cast<bitmask_t>(1) << enum_to_bit(value)); }
|
||||||
|
|
||||||
/// Add multiple enum values from initializer list
|
/// Add multiple enum values from initializer list
|
||||||
constexpr void add(std::initializer_list<EnumType> values) {
|
constexpr void insert(std::initializer_list<EnumType> values) {
|
||||||
for (auto value : values) {
|
for (auto value : values) {
|
||||||
this->add(value);
|
this->insert(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// std::set compatibility: insert() is an alias for add()
|
/// Remove an enum value from the set (std::set compatibility)
|
||||||
constexpr void insert(EnumType value) { this->add(value); }
|
constexpr void erase(EnumType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << enum_to_bit(value)); }
|
||||||
|
|
||||||
/// Remove an enum value from the set
|
|
||||||
constexpr void remove(EnumType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << enum_to_bit(value)); }
|
|
||||||
|
|
||||||
/// std::set compatibility: erase() is an alias for remove()
|
|
||||||
constexpr void erase(EnumType value) { this->remove(value); }
|
|
||||||
|
|
||||||
/// Clear all values from the set
|
/// Clear all values from the set
|
||||||
constexpr void clear() { this->mask_ = 0; }
|
constexpr void clear() { this->mask_ = 0; }
|
||||||
|
|
||||||
/// Check if the set contains a specific enum value
|
/// Check if the set contains a specific enum value (std::set compatibility)
|
||||||
constexpr bool contains(EnumType value) const {
|
/// Returns 1 if present, 0 if not (same as std::set for unique elements)
|
||||||
return (this->mask_ & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0;
|
constexpr size_t count(EnumType value) const {
|
||||||
|
return (this->mask_ & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// std::set compatibility: count() returns 1 if present, 0 if not (same as std::set for unique elements)
|
|
||||||
constexpr size_t count(EnumType value) const { return this->contains(value) ? 1 : 0; }
|
|
||||||
|
|
||||||
/// Count the number of enum values in the set
|
/// Count the number of enum values in the set
|
||||||
constexpr size_t size() const {
|
constexpr size_t size() const {
|
||||||
// Brian Kernighan's algorithm - efficient for sparse bitmasks
|
// Brian Kernighan's algorithm - efficient for sparse bitmasks
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# What does this implement/fix?
|
|
||||||
|
|
||||||
This PR extracts the `ColorModeMask` implementation from the light component into a generic `EnumBitmask<T, MaxBits>` template helper in `esphome/core/enum_bitmask.h`. This refactoring enables code reuse across other components (e.g., climate, fan) that need efficient enum set storage without STL container overhead.
|
|
||||||
|
|
||||||
## Key Benefits
|
|
||||||
|
|
||||||
- **Code Reuse**: Generic template can be used by any component needing enum bitmask storage (climate, fan, cover, etc.)
|
|
||||||
- **Memory Efficiency**: Replaces `std::set<EnumType>` with compact bitmask storage (~586 bytes saved per instance)
|
|
||||||
- **Zero-cost Abstraction**: Maintains same performance characteristics with cleaner, more maintainable code
|
|
||||||
- **Flash Savings**: 16 bytes reduction on ESP8266 in initial testing
|
|
||||||
|
|
||||||
## Technical Changes
|
|
||||||
|
|
||||||
1. **New Generic Template** (`esphome/core/enum_bitmask.h`):
|
|
||||||
- `EnumBitmask<EnumType, MaxBits>` template class
|
|
||||||
- Auto-selects optimal storage type (uint8_t/uint16_t/uint32_t) based on MaxBits
|
|
||||||
- Provides iterator support, initializer list construction, and static utility methods
|
|
||||||
- Requires specialization of `enum_to_bit()` and `bit_to_enum()` for each enum type
|
|
||||||
|
|
||||||
2. **std::set Compatibility**:
|
|
||||||
- Provides both modern API (`.contains()`, `.add()`, `.remove()`) and std::set-compatible aliases (`.count()`, `.insert()`, `.erase()`)
|
|
||||||
- True drop-in replacement - existing code using `.insert()` and `.count()` works unchanged
|
|
||||||
|
|
||||||
3. **Light Component Refactoring** (`esphome/components/light/color_mode.h`):
|
|
||||||
- Replaced custom `ColorModeMask` class with `using ColorModeMask = EnumBitmask<ColorMode, 10>`
|
|
||||||
- Single shared `COLOR_MODE_LOOKUP` array eliminates code duplication
|
|
||||||
- Template specializations provide enum↔bit mapping
|
|
||||||
- Moved `has_capability()` to namespace-level function for cleaner API
|
|
||||||
|
|
||||||
4. **Updated Call Sites**:
|
|
||||||
- `light_call.cpp`: Uses `ColorModeMask::first_value_from_mask()` and `ColorModeMask::mask_contains()` static methods
|
|
||||||
- `light_traits.h`: Uses namespace-level `has_capability()` function
|
|
||||||
- No changes required to other light component files (drop-in replacement)
|
|
||||||
|
|
||||||
## Design Rationale
|
|
||||||
|
|
||||||
The generic template follows the same pattern as the original `ColorModeMask` but makes it reusable:
|
|
||||||
- Constexpr-compatible for compile-time initialization
|
|
||||||
- Iterator support for range-based for loops and API encoding
|
|
||||||
- Static methods for working with raw bitmask values (for bitwise operation results)
|
|
||||||
- Protected specialization interface ensures type safety
|
|
||||||
|
|
||||||
This establishes a pattern that can be applied to other components:
|
|
||||||
- Climate modes/presets (upcoming PR)
|
|
||||||
- Fan modes
|
|
||||||
- Cover operations
|
|
||||||
- Any component with small enum sets (≤32 values)
|
|
||||||
|
|
||||||
## Types of changes
|
|
||||||
|
|
||||||
- [x] Code quality improvements to existing code or addition of tests
|
|
||||||
|
|
||||||
**Related issue or feature (if applicable):**
|
|
||||||
|
|
||||||
- Part of ongoing memory optimization effort for embedded platforms
|
|
||||||
|
|
||||||
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
|
||||||
|
|
||||||
- N/A (internal refactoring, no user-facing changes)
|
|
||||||
|
|
||||||
## Test Environment
|
|
||||||
|
|
||||||
- [x] ESP32
|
|
||||||
- [x] ESP32 IDF
|
|
||||||
- [x] ESP8266
|
|
||||||
- [ ] RP2040
|
|
||||||
- [ ] BK72xx
|
|
||||||
- [ ] RTL87xx
|
|
||||||
- [ ] nRF52840
|
|
||||||
|
|
||||||
## Example entry for `config.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# No config changes required - internal refactoring only
|
|
||||||
# All existing light configurations continue to work unchanged
|
|
||||||
|
|
||||||
light:
|
|
||||||
- platform: rgb
|
|
||||||
id: test_rgb_light
|
|
||||||
name: "Test RGB Light"
|
|
||||||
red: red_output
|
|
||||||
green: green_output
|
|
||||||
blue: blue_output
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checklist:
|
|
||||||
- [x] The code change is tested and works locally.
|
|
||||||
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
|
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
|
||||||
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
|
||||||
|
|
||||||
## Additional Notes
|
|
||||||
|
|
||||||
- **Zero functional changes**: This is a pure refactoring with identical runtime behavior
|
|
||||||
- **Binary size impact**: Slight improvement on ESP8266 (16 bytes flash reduction)
|
|
||||||
- **Future work**: Will apply this pattern to climate component in follow-up PR
|
|
||||||
- **Test coverage**: All modified code covered by existing light component tests
|
|
||||||
Reference in New Issue
Block a user