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