diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index a749cd7305..de8f892b04 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -10,10 +10,19 @@ namespace esphome::light { static const char *const TAG = "light"; -// Helper functions to reduce code size for logging -static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f, - float max = 1.0f) { +// Helper functions to reduce code size for logging. +// +// `param_name_progmem` is a pointer to a flash-resident `const LogString *` +// slot (e.g. into the FIELD_NAMES table below). We only dereference it via +// `progmem_read_ptr` on the cold path that actually emits the log message, +// so the hot path (value in range) performs no flash reads at all. On +// non-ESP8266 platforms `progmem_read_ptr` is a plain `*addr` inline, so +// there is no cost there either. +static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *const *param_name_progmem, + float min = 0.0f, float max = 1.0f) { if (value < min || value > max) { + const auto *param_name = reinterpret_cast( + progmem_read_ptr(reinterpret_cast(param_name_progmem))); ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max); value = clamp(value, min, max); } @@ -277,25 +286,60 @@ LightColorValues LightCall::validate_() { if (this->has_state()) v.set_state(this->state_); - // clamp_and_log_if_invalid already clamps in-place, so assign directly - // to avoid redundant clamp code from the setter being inlined. -#define VALIDATE_AND_APPLY(field, name_str, ...) \ - if (this->has_##field()) { \ - clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \ - v.field##_ = this->field##_; \ + // Clamp the eight [0.0, 1.0] fields and copy them from `this` into `v`. + // + // LightCall and LightColorValues both declare the same eight float fields in + // the same order (brightness_, color_brightness_, red_, green_, blue_, + // white_, cold_white_, warm_white_), and their corresponding flag bits are + // also 0-7 in that order. Under that layout the LightCall offset for field i + // is `offsetof(LightCall, brightness_) + i * 4`, and the LightColorValues + // offset is exactly 12 bytes lower (enforced by the static_asserts below). + // Iterating via bit-position arithmetic lets us collapse eight inlined + // clamp/copy blocks into a single loop. + static_assert(FLAG_HAS_BRIGHTNESS == 1u << 0, "clamp loop assumes bit 0"); + static_assert(FLAG_HAS_WARM_WHITE == 1u << 7, "clamp loop assumes bit 7"); + static_assert(offsetof(LightCall, warm_white_) - offsetof(LightCall, brightness_) == 7 * sizeof(float), + "LightCall clamp fields must be contiguous"); + static_assert(offsetof(LightColorValues, warm_white_) - offsetof(LightColorValues, brightness_) == 7 * sizeof(float), + "LightColorValues clamp fields must be contiguous"); + static_assert(offsetof(LightCall, brightness_) - offsetof(LightColorValues, brightness_) == 12, + "LightCall and LightColorValues clamp fields must have constant byte-offset delta"); + + static const LogString *const FIELD_NAMES[8] PROGMEM = { + LOG_STR("Brightness"), // FLAG_HAS_BRIGHTNESS (bit 0) + LOG_STR("Color brightness"), // FLAG_HAS_COLOR_BRIGHTNESS (bit 1) + LOG_STR("Red"), // FLAG_HAS_RED (bit 2) + LOG_STR("Green"), // FLAG_HAS_GREEN (bit 3) + LOG_STR("Blue"), // FLAG_HAS_BLUE (bit 4) + LOG_STR("White"), // FLAG_HAS_WHITE (bit 5) + LOG_STR("Cold white"), // FLAG_HAS_COLD_WHITE (bit 6) + LOG_STR("Warm white"), // FLAG_HAS_WARM_WHITE (bit 7) + }; + constexpr size_t SRC_BASE = offsetof(LightCall, brightness_); + constexpr size_t SRC_TO_DST_DELTA = SRC_BASE - offsetof(LightColorValues, brightness_); + + uint8_t active = this->flags_ & CLAMP_FLAGS_MASK; + if (active != 0) { + auto *self = reinterpret_cast(this); + auto *out = reinterpret_cast(&v); + for (uint8_t bit = 0; bit < 8; bit++) { + if (!(active & (1u << bit))) + continue; + const size_t src_off = SRC_BASE + bit * sizeof(float); + float &f = *reinterpret_cast(self + src_off); + clamp_and_log_if_invalid(name, f, &FIELD_NAMES[bit]); + *reinterpret_cast(out + src_off - SRC_TO_DST_DELTA) = f; + } } - VALIDATE_AND_APPLY(brightness, "Brightness") - VALIDATE_AND_APPLY(color_brightness, "Color brightness") - VALIDATE_AND_APPLY(red, "Red") - VALIDATE_AND_APPLY(green, "Green") - VALIDATE_AND_APPLY(blue, "Blue") - VALIDATE_AND_APPLY(white, "White") - VALIDATE_AND_APPLY(cold_white, "Cold white") - VALIDATE_AND_APPLY(warm_white, "Warm white") - VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) - -#undef VALIDATE_AND_APPLY + // color_temperature uses a dynamic range from the light's traits and is + // handled separately. + if (this->has_color_temperature()) { + static const LogString *const CT_NAME PROGMEM = LOG_STR("Color temperature"); + clamp_and_log_if_invalid(name, this->color_temperature_, &CT_NAME, traits.get_min_mireds(), + traits.get_max_mireds()); + v.color_temperature_ = this->color_temperature_; + } v.normalize_color(); diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 88d29bd349..9f34297dcd 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -196,24 +196,31 @@ class LightCall { void transform_parameters_(const LightTraits &traits); // Bitfield flags - each flag indicates whether a corresponding value has been set. + // + // Bits 0-7 are the eight float fields that share the [0.0, 1.0] clamp range, + // in member declaration order. The validate_() clamp loop relies on this + // layout to index into LightCall/LightColorValues via bit-position arithmetic + // without a per-field offset table. Do not reorder without updating the + // static_asserts in light_call.cpp. enum FieldFlags : uint16_t { - FLAG_HAS_STATE = 1 << 0, - FLAG_HAS_TRANSITION = 1 << 1, - FLAG_HAS_FLASH = 1 << 2, - FLAG_HAS_EFFECT = 1 << 3, - FLAG_HAS_BRIGHTNESS = 1 << 4, - FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5, - FLAG_HAS_RED = 1 << 6, - FLAG_HAS_GREEN = 1 << 7, - FLAG_HAS_BLUE = 1 << 8, - FLAG_HAS_WHITE = 1 << 9, - FLAG_HAS_COLOR_TEMPERATURE = 1 << 10, - FLAG_HAS_COLD_WHITE = 1 << 11, - FLAG_HAS_WARM_WHITE = 1 << 12, + FLAG_HAS_BRIGHTNESS = 1 << 0, + FLAG_HAS_COLOR_BRIGHTNESS = 1 << 1, + FLAG_HAS_RED = 1 << 2, + FLAG_HAS_GREEN = 1 << 3, + FLAG_HAS_BLUE = 1 << 4, + FLAG_HAS_WHITE = 1 << 5, + FLAG_HAS_COLD_WHITE = 1 << 6, + FLAG_HAS_WARM_WHITE = 1 << 7, + FLAG_HAS_COLOR_TEMPERATURE = 1 << 8, + FLAG_HAS_STATE = 1 << 9, + FLAG_HAS_TRANSITION = 1 << 10, + FLAG_HAS_FLASH = 1 << 11, + FLAG_HAS_EFFECT = 1 << 12, FLAG_HAS_COLOR_MODE = 1 << 13, FLAG_PUBLISH = 1 << 14, FLAG_SAVE = 1 << 15, }; + static constexpr uint16_t CLAMP_FLAGS_MASK = 0x00FFu; // bits 0-7 inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } @@ -239,7 +246,13 @@ class LightCall { LightState *parent_; // Light state values - use flags_ to check if a value has been set. - // Group 4-byte aligned members first + // Group 4-byte aligned members first. + // + // The eight [0.0, 1.0]-clamped float fields (brightness_ ... warm_white_) + // are declared in the same order as their flag bits (0-7) and the matching + // fields in LightColorValues. validate_() exploits this to iterate them via + // bit-position arithmetic. color_temperature_ has a custom range and lives + // outside that block. uint32_t transition_length_; uint32_t flash_length_; uint32_t effect_; @@ -249,9 +262,9 @@ class LightCall { float green_; float blue_; float white_; - float color_temperature_; float cold_white_; float warm_white_; + float color_temperature_; // Smaller members at the end for better packing uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index fa286a3941..c520f4dc25 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -52,9 +52,9 @@ class LightColorValues { green_(1.0f), blue_(1.0f), white_(1.0f), - color_temperature_{0.0f}, cold_white_{1.0f}, warm_white_{1.0f}, + color_temperature_{0.0f}, color_mode_(ColorMode::UNKNOWN) {} LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, @@ -287,6 +287,12 @@ class LightColorValues { friend class LightCall; protected: + // The eight [0.0, 1.0]-clamped float fields are declared in the same order + // as their flag bits (0-7) in LightCall::FieldFlags and the matching fields + // in LightCall. LightCall::validate_() exploits this layout to iterate and + // copy them via bit-position arithmetic with a constant delta of 12 bytes + // between matching LightCall and LightColorValues members. color_temperature_ + // has a different range and is placed after the clamp block. float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -294,9 +300,9 @@ class LightColorValues { float green_; float blue_; float white_; - float color_temperature_; ///< Color Temperature in Mired float cold_white_; float warm_white_; + float color_temperature_; ///< Color Temperature in Mired ColorMode color_mode_; };