1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-30 06:33:51 +00:00

[light] Extract ColorModeMask into generic FiniteSetMask helper (#11472)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2025-10-22 15:22:46 -10:00
committed by GitHub
parent 6efe346cc5
commit 2864e989bd
4 changed files with 237 additions and 155 deletions

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include "esphome/core/finite_set_mask.h"
namespace esphome {
namespace light {
@@ -107,13 +108,9 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
// Type alias for raw color mode bitmask values
using color_mode_bitmask_t = uint16_t;
// Constants for ColorMode count and bit range
static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type
// Compile-time array of all ColorMode values in declaration order
// Bit positions (0-9) map directly to enum declaration order
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
// Lookup table for ColorMode bit mapping
// This array defines the canonical order of color modes (bit 0-9)
constexpr ColorMode COLOR_MODE_LOOKUP[] = {
ColorMode::UNKNOWN, // bit 0
ColorMode::ON_OFF, // bit 1
ColorMode::BRIGHTNESS, // bit 2
@@ -126,33 +123,42 @@ static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
};
/// Map ColorMode enum values to bit positions (0-9)
/// Bit positions follow the enum declaration order
static constexpr int mode_to_bit(ColorMode mode) {
// Linear search through COLOR_MODES array
// Compiler optimizes this to efficient code since array is constexpr
for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
if (COLOR_MODES[i] == mode)
return i;
}
return 0;
}
/// Bit mapping policy for ColorMode
/// Uses lookup table for non-contiguous enum values
struct ColorModeBitPolicy {
using mask_t = uint16_t; // 10 bits requires uint16_t
static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
/// Map bit positions (0-9) to ColorMode enum values
/// Bit positions follow the enum declaration order
static constexpr ColorMode bit_to_mode(int bit) {
// Direct lookup in COLOR_MODES array
return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
}
static constexpr unsigned to_bit(ColorMode mode) {
// Linear search through lookup table
// Compiler optimizes this to efficient code since array is constexpr
for (int i = 0; i < MAX_BITS; ++i) {
if (COLOR_MODE_LOOKUP[i] == mode)
return i;
}
return 0;
}
static constexpr ColorMode from_bit(unsigned bit) {
return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN;
}
};
// Type alias for ColorMode bitmask using policy-based design
using ColorModeMask = FiniteSetMask<ColorMode, ColorModeBitPolicy>;
// Number of ColorCapability enum values
constexpr int COLOR_CAPABILITY_COUNT = 6;
/// Helper to compute capability bitmask at compile time
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
color_mode_bitmask_t mask = 0;
constexpr uint16_t compute_capability_bitmask(ColorCapability capability) {
uint16_t mask = 0;
uint8_t cap_bit = static_cast<uint8_t>(capability);
// Check each ColorMode to see if it has this capability
for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
for (int bit = 0; bit < color_mode_count; ++bit) {
uint8_t mode_val = static_cast<uint8_t>(COLOR_MODE_LOOKUP[bit]);
if ((mode_val & cap_bit) != 0) {
mask |= (1 << bit);
}
@@ -160,12 +166,9 @@ static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability
return mask;
}
// Number of ColorCapability enum values
static constexpr int COLOR_CAPABILITY_COUNT = 6;
/// Compile-time lookup table mapping ColorCapability to bitmask
/// This array is computed at compile time using constexpr
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
constexpr uint16_t CAPABILITY_BITMASKS[] = {
compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0
compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1
compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2
@@ -174,130 +177,38 @@ static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
};
/// Bitmask for storing a set of ColorMode values efficiently.
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
class ColorModeMask {
public:
constexpr ColorModeMask() = default;
/// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
/// Add multiple modes at once using initializer list
constexpr void add(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
constexpr size_t size() const {
// Count set bits using Brian Kernighan's algorithm
// More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
uint16_t n = this->mask_;
size_t count = 0;
while (n) {
n &= n - 1; // Clear the least significant set bit
count++;
}
return count;
}
constexpr bool empty() const { return this->mask_ == 0; }
/// Iterator support for API encoding
class Iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = ColorMode;
using difference_type = std::ptrdiff_t;
using pointer = const ColorMode *;
using reference = ColorMode;
constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
constexpr Iterator &operator++() {
++bit_;
advance_to_next_set_bit_();
return *this;
}
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
private:
constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
color_mode_bitmask_t mask_;
int bit_;
};
constexpr Iterator begin() const { return Iterator(mask_, 0); }
constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
/// Get the raw bitmask value for API encoding
constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
/// Find the next set bit in a bitmask starting from a given position
/// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
int bit = start_bit;
while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
++bit;
}
return bit;
}
/// Find the first set bit in a bitmask and return the corresponding ColorMode
/// Used for optimizing compute_color_mode_() intersection logic
static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
return bit_to_mode(find_next_set_bit(mask, 0));
}
/// Check if a ColorMode is present in a raw bitmask value
/// Useful for checking intersection results without creating a temporary ColorModeMask
static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
return (mask & (1 << mode_to_bit(mode))) != 0;
}
/// Check if any mode in the bitmask has a specific capability
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
bool has_capability(ColorCapability capability) const {
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
// We need to convert the power-of-2 value to an index
uint8_t cap_val = static_cast<uint8_t>(capability);
/**
* @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS
* lookup.
*
* This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5).
* Used to index into the CAPABILITY_BITMASKS lookup table.
*
* @param capability A ColorCapability enum value (must be a power of 2).
* @return The corresponding array index (0-based).
*/
inline int capability_to_index(ColorCapability capability) {
uint8_t cap_val = static_cast<uint8_t>(capability);
#if defined(__GNUC__) || defined(__clang__)
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
int index = __builtin_ctz(cap_val);
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
return __builtin_ctz(cap_val);
#else
// Fallback for compilers without __builtin_ctz
int index = 0;
while (cap_val > 1) {
cap_val >>= 1;
++index;
}
#endif
return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
// Fallback for compilers without __builtin_ctz
int index = 0;
while (cap_val > 1) {
cap_val >>= 1;
++index;
}
return index;
#endif
}
private:
// Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
// Currently only 10 ColorMode values exist, so 16 bits is sufficient.
// Can be changed to uint32_t if more than 16 color modes are needed in the future.
// Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
color_mode_bitmask_t mask_{0};
};
/// Check if any mode in the bitmask has a specific capability
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) {
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0;
}
} // namespace light
} // namespace esphome

View File

@@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() {
// Use the preferred suitable mode.
if (intersection != 0) {
ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
ColorMode mode = ColorModeMask::first_value_from_mask(intersection);
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(mode)));
return mode;

View File

@@ -26,9 +26,9 @@ class LightTraits {
this->supported_color_modes_ = ColorModeMask(modes);
}
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; }
bool supports_color_capability(ColorCapability color_capability) const {
return this->supported_color_modes_.has_capability(color_capability);
return has_capability(this->supported_color_modes_, color_capability);
}
float get_min_mireds() const { return this->min_mireds_; }

View File

@@ -0,0 +1,171 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <iterator>
#include <type_traits>
namespace esphome {
/// Default bit mapping policy for contiguous enums starting at 0
/// Provides 1:1 mapping where enum value equals bit position
template<typename ValueType, int MaxBits> struct DefaultBitPolicy {
// Automatic bitmask type selection based on MaxBits
// ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t,
typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type;
static constexpr int MAX_BITS = MaxBits;
static constexpr unsigned to_bit(ValueType value) { return static_cast<unsigned>(value); }
static constexpr ValueType from_bit(unsigned bit) { return static_cast<ValueType>(bit); }
};
/// Generic bitmask for storing a finite set of discrete values efficiently.
/// Replaces std::set<ValueType> to eliminate red-black tree overhead (~586 bytes per instantiation).
///
/// Template parameters:
/// ValueType: The type to store (typically enum, but can be any discrete bounded type)
/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy)
///
/// BitPolicy requirements:
/// - using mask_t = <uint8_t|uint16_t|uint32_t> // Bitmask storage type
/// - static constexpr int MAX_BITS // Maximum number of bits
/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position
/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value
///
/// Example usage (1:1 mapping - climate enums):
/// // For contiguous enums starting at 0, use DefaultBitPolicy
/// using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
/// if (modes.count(CLIMATE_MODE_HEAT)) { ... }
/// for (auto mode : modes) { ... }
///
/// Example usage (custom mapping - ColorMode):
/// // For custom mappings, define a custom BitPolicy
/// // See esphome/components/light/color_mode.h for complete example
///
/// Design notes:
/// - Policy-based design allows custom bit mappings without template specialization
/// - Iterator converts bit positions to actual values during traversal
/// - All operations are constexpr-compatible for compile-time initialization
/// - Drop-in replacement for std::set<ValueType> with simpler API
///
template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16>> class FiniteSetMask {
public:
using bitmask_t = typename BitPolicy::mask_t;
constexpr FiniteSetMask() = default;
/// Construct from initializer list: {VALUE1, VALUE2, ...}
constexpr FiniteSetMask(std::initializer_list<ValueType> values) {
for (auto value : values) {
this->insert(value);
}
}
/// Add a single value to the set (std::set compatibility)
constexpr void insert(ValueType value) { this->mask_ |= (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value)); }
/// Add multiple values from initializer list
constexpr void insert(std::initializer_list<ValueType> values) {
for (auto value : values) {
this->insert(value);
}
}
/// Remove a value from the set (std::set compatibility)
constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << BitPolicy::to_bit(value)); }
/// Clear all values from the set
constexpr void clear() { this->mask_ = 0; }
/// Check if the set contains a specific value (std::set compatibility)
/// Returns 1 if present, 0 if not (same as std::set for unique elements)
constexpr size_t count(ValueType value) const {
return (this->mask_ & (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0;
}
/// Count the number of values in the set
constexpr size_t size() const {
// Brian Kernighan's algorithm - efficient for sparse bitmasks
// Typical case: 2-4 modes out of 10 possible
bitmask_t n = this->mask_;
size_t count = 0;
while (n) {
n &= n - 1; // Clear the least significant set bit
count++;
}
return count;
}
/// Check if the set is empty
constexpr bool empty() const { return this->mask_ == 0; }
/// Iterator support for range-based for loops and API encoding
/// Iterates over set bits and converts bit positions to values
/// Optimization: removes bits from mask as we iterate
class Iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = ValueType;
using difference_type = std::ptrdiff_t;
using pointer = const ValueType *;
using reference = ValueType;
constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {}
constexpr ValueType operator*() const {
// Return value for the first set bit
return BitPolicy::from_bit(find_next_set_bit(mask_, 0));
}
constexpr Iterator &operator++() {
// Clear the lowest set bit (Brian Kernighan's algorithm)
mask_ &= mask_ - 1;
return *this;
}
constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; }
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
private:
bitmask_t mask_;
};
constexpr Iterator begin() const { return Iterator(mask_); }
constexpr Iterator end() const { return Iterator(0); }
/// Get the raw bitmask value for optimized operations
constexpr bitmask_t get_mask() const { return this->mask_; }
/// Check if a specific value is present in a raw bitmask
/// Useful for checking intersection results without creating temporary objects
static constexpr bool mask_contains(bitmask_t mask, ValueType value) {
return (mask & (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value))) != 0;
}
/// Get the first value from a raw bitmask
/// Used for optimizing intersection logic (e.g., "pick first suitable mode")
static constexpr ValueType first_value_from_mask(bitmask_t mask) {
return BitPolicy::from_bit(find_next_set_bit(mask, 0));
}
/// Find the next set bit in a bitmask starting from a given position
/// Returns the bit position, or MAX_BITS if no more bits are set
static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) {
int bit = start_bit;
while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast<bitmask_t>(1) << bit))) {
++bit;
}
return bit;
}
protected:
bitmask_t mask_{0};
};
} // namespace esphome