1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-26 04:33:47 +00:00
This commit is contained in:
J. Nick Koston
2025-10-21 22:25:57 -10:00
parent 1119b4e11e
commit f8f967b25c
3 changed files with 12 additions and 318 deletions

View File

@@ -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)

View File

@@ -23,7 +23,7 @@ namespace esphome {
/// Example usage:
/// using ClimateModeMask = EnumBitmask<ClimateMode, 8>;
/// 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 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, ...}
constexpr EnumBitmask(std::initializer_list<EnumType> values) {
for (auto value : values) {
this->add(value);
this->insert(value);
}
}
/// Add a single enum value to the set
constexpr void add(EnumType value) { this->mask_ |= (static_cast<bitmask_t>(1) << enum_to_bit(value)); }
/// Add a single enum value to the set (std::set compatibility)
constexpr void insert(EnumType value) { this->mask_ |= (static_cast<bitmask_t>(1) << enum_to_bit(value)); }
/// 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) {
this->add(value);
this->insert(value);
}
}
/// std::set compatibility: insert() is an alias for add()
constexpr void insert(EnumType value) { this->add(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); }
/// Remove an enum value from the set (std::set compatibility)
constexpr void erase(EnumType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << enum_to_bit(value)); }
/// Clear all values from the set
constexpr void clear() { this->mask_ = 0; }
/// Check if the set contains a specific enum value
constexpr bool contains(EnumType value) const {
return (this->mask_ & (static_cast<bitmask_t>(1) << enum_to_bit(value))) != 0;
/// Check if the set contains a specific enum value (std::set compatibility)
/// Returns 1 if present, 0 if not (same as std::set for unique elements)
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
constexpr size_t size() const {
// Brian Kernighan's algorithm - efficient for sparse bitmasks

View File

@@ -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