From 1119b4e11e21636f02e123053ad043054fae2319 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Oct 2025 22:23:37 -1000 Subject: [PATCH] [core] Add std::set compatibility aliases to EnumBitmask - Add insert() as alias for add() - Add erase() as alias for remove() - Add count() as alias for contains() - Makes EnumBitmask a true drop-in replacement for std::set - Update documentation to reflect compatibility --- enum_templates.md | 200 +++++++++++++++++++++++++++ esphome/core/enum_bitmask.h | 9 ++ extract_color_mode_mask_helper_pr.md | 98 +++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 enum_templates.md create mode 100644 extract_color_mode_mask_helper_pr.md diff --git a/enum_templates.md b/enum_templates.md new file mode 100644 index 0000000000..175f8d0b89 --- /dev/null +++ b/enum_templates.md @@ -0,0 +1,200 @@ +# EnumBitmask Pattern Documentation + +## Overview + +`EnumBitmask` from `esphome/core/enum_bitmask.h` provides a memory-efficient replacement for `std::set` when storing sets of enum values. + +## When to Use + +Use `EnumBitmask` instead of `std::set` 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 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; +``` + +### 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::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::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(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` 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) diff --git a/esphome/core/enum_bitmask.h b/esphome/core/enum_bitmask.h index fdbd0c50cc..d5d531763e 100644 --- a/esphome/core/enum_bitmask.h +++ b/esphome/core/enum_bitmask.h @@ -62,9 +62,15 @@ template class EnumBitmask { } } + /// 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(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 constexpr void clear() { this->mask_ = 0; } @@ -73,6 +79,9 @@ template class EnumBitmask { return (this->mask_ & (static_cast(1) << enum_to_bit(value))) != 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 diff --git a/extract_color_mode_mask_helper_pr.md b/extract_color_mode_mask_helper_pr.md new file mode 100644 index 0000000000..6a4d98a5f8 --- /dev/null +++ b/extract_color_mode_mask_helper_pr.md @@ -0,0 +1,98 @@ +# What does this implement/fix? + +This PR extracts the `ColorModeMask` implementation from the light component into a generic `EnumBitmask` 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` 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` 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` + - 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