Since validate_() now accesses unit_fields_[] directly via the union
alias, there's no offsetof arithmetic to guard. The FieldFlags bit
layout assert is all that's needed.
Replaces the previous `float *p = &this->brightness_; p[bit]` pattern
(flagged by Copilot as pointer-into-scalar UB) with a shared anonymous
union that exposes brightness_..warm_white_ as unit_fields_[8] in both
LightCall and LightColorValues. validate_() now indexes the real array
directly — defined behavior — while the named members stay accessible
to getters/setters. Code generation is unchanged.
The union is declared once via ESPHOME_LIGHT_UNIT_FIELDS_UNION() in
light_color_values.h and expanded in both structs. The per-field
offsetof static_asserts collapse to a single FieldFlags bit-layout
assert since the union guarantees member ↔ array-index alignment.
Caught by Copilot on PR review.
-0.0f has bit pattern 0x80000000 which exceeds ONE_F_BITS as unsigned,
so the check previously flagged it and emitted a spurious out-of-range
warning. Add an explicit compare against NEG_ZERO_F_BITS (declared as
a named constexpr for readability) so -0.0f takes the in-range fast
path. clamp_unit_float() already returned 0.0f for it via the sign-bit
branch, so behavior on clamp is unchanged.
Caught by Copilot on PR review.
The PROGMEM_STRING_TABLE expansion is entirely constexpr-initialized, so it doesn't require the per-static thread-safe-init guard variables that the previous `static const LogString *const FIELD_NAMES[8] PROGMEM = { LOG_STR(...) }` form emitted inside validate_() (LOG_STR contains a GCC statement-expression, which is not a constant expression, so GCC fell back to dynamic init with an 8-byte guard).
Combined the 8 clamp-field names and "Color temperature" into a single ValidateFieldNames table; index 8 is reserved for CT. log_value_out_of_range_() now takes the resolved `const LogString *` directly, so the helper no longer needs its internal progmem_read_ptr dereference.
Impact vs. prior commit (isolated light build):
ESP8266 .irom0.text: 267912 -> 267784 (-128 B)
ESP8266 .bss guard vars on light symbols: 16 -> 0 (-16 B RAM)
ESP32-IDF .flash.text: 136988 -> 136996 (+8 B, marginal)
Move `float_out_of_unit_range()` / add `clamp_unit_float()` to
light_color_values.h so the nine `set_*(float)` setters can use the
unsigned bit-pattern clamp instead of `std::clamp(x, 0.0f, 1.0f)`.
`std::clamp` expands to two soft-float `__ltsf2`/`__gtsf2` calls per
invocation on ESP8266 — replacing it with a single unsigned compare
saves code across every caller of these setters (StrobeLightEffect,
the 11-arg LightColorValues constructor, external components).
Split the cold-path helper: `log_value_out_of_range_()` now only logs,
and each caller applies the clamp strategy appropriate to its range
(`clamp_unit_float` for the 8-field loop, `std::clamp` for color
temperature's runtime-variable range).
Add layout/format assertions:
- std::is_standard_layout_v on LightCall and LightColorValues so the
offsetof arithmetic in the clamp loop is well-defined.
- sizeof(float) == 4 and is_iec559 so the bit-pattern trick is valid.
A direct __builtin_bit_cast check would be cleaner but is not
available on the ESP8266 xtensa toolchain.
Text-section delta vs. prior commit (isolated light build):
ESP32-IDF: .flash.text 137760 -> 136988 (-772 B)
ESP8266: .irom0.text 268440 -> 267912 (-528 B)
The inlined `value < 0.0f || value > 1.0f` check in the clamp loop costs
~50 B per iteration on ESP8266 (two libgcc soft-float calls with register
spills). IEEE 754 floats in [0.0f, 1.0f] have bit patterns in
[0x00000000, 0x3F800000]; anything out of range — values > 1.0f, negatives
(sign bit set → huge unsigned interpretation), NaN, Infinity — has a
strictly larger unsigned interpretation. A single `pun.u > 0x3F800000u`
covers every case.
Using a union for the type-pun rather than memcpy/bit_cast because those
don't optimize to a no-op on xtensa-gcc (same reason api/proto.h's
float_to_raw() uses a union).
The loop body is now two instructions for the range check:
l32i a2, a9, 0 ; load raw u32
bgeu a10, a2, ... ; compare against pre-hoisted 1.0f bits
Size delta vs. the prior float-compare form:
ESP32-IDF: validate_ -16 B, net -12 B
ESP8266: validate_ -20 B, net -20 B
vs. dev baseline (isolated light build, matched funcs):
ESP32-IDF: -103 B code, +32 B PROGMEM table = -71 B net
ESP8266: -42 B code, +32 B PROGMEM table = -10 B net
Follow-up to edb2145a addressing three points:
1. Hoist the in-range check out of the logging helper. The loop now tests
`value < 0.0f || value > 1.0f` inline and only calls the out-of-line
log_out_of_range_and_clamp_ helper on the cold path. Hot path (value in
range) skips the call8 and the register spill/reload around it, which
matters because HA automations can drive perform() at high frequency.
2. Iterate only set bits with __builtin_ctz + (active & active-1). Common
calls with one or two flags set now exit the loop after one or two
iterations instead of always scanning all eight slots.
3. Replace the uint8_t* pointer arithmetic with typed float arrays aliasing
&brightness_ in each struct. Per-field static_asserts (expanded via a
local macro) now catch reorders of any single member in either struct,
not just reorders at the endpoints. Compiles to the same machine code as
the uint8_t* version.
Size delta vs. prior commit (isolated light build):
ESP32-IDF: validate_ +60 B, helper -29 B, net +31 B
ESP8266: validate_ +72 B, helper -42 B, net +30 B
Still a net win vs. dev on both targets (ESP32-IDF -91 B, ESP8266 -22 B).
Reorder FieldFlags so the eight [0.0, 1.0]-clamped float fields occupy
bits 0-7 in the same order as they appear in LightCall and
LightColorValues, and move color_temperature_ to the end of both
structs. Under that layout the LightCall offset for clamp field i is
`offsetof(LightCall, brightness_) + i * 4`, and the LightColorValues
offset is exactly 12 bytes lower for every field. validate_() now
iterates the active clamp bits in a small loop that computes these
offsets from the bit position instead of expanding eight nearly
identical inline blocks via macro.
The field-name PROGMEM pointer is passed to clamp_and_log_if_invalid as
`const LogString *const *`; progmem_read_ptr only runs on the cold
(out-of-range) path, so the hot path performs no flash reads for the
name. The eight invariants the loop relies on (flag-bit layout,
field contiguity, and the constant 12-byte delta) are enforced by
static_asserts so any future reshuffle fails loudly at compile time.
Size deltas for the isolated light component build (vs dev):
ESP32-IDF: -118 B code, +32 B PROGMEM name table = -86 B net
ESP8266: -56 B code, +32 B PROGMEM name table = -24 B net
0 B RAM impact on both targets.
When max_brightness and local_brightness_ are small but non-zero, the
intermediate (uncorrected / max_brightness) * 255 can exceed 65535
before the std::min(255) clamp runs, producing an incorrect low result.
Widen intermediates to uint32_t. Copilot review catch on #15727.
- mock_addressable_light.h: add direct <memory>/<cstdint>/<cstddef> includes
- test: use asyncio.get_running_loop() instead of deprecated get_event_loop()
- test: rebase timing to command-issue time (not first-nonzero) and use
absolute progress for assertion 2, so late-transition check can't skew
when the first nonzero sample happens to land near the assertion-1 limit
The color_uncorrect_red/green/blue/white helpers each contain two 16-bit
divides and a call into the out-of-line gamma_uncorrect_ LUT search. They
were marked ESPHOME_ALWAYS_INLINE, a hint inherited from the original 2019
C++ port when the gamma math was a single powf() call and the methods were
trivially small.
Since the 16-bit gamma LUT landed (#14123, Feb 2026) the bodies grew enough
that forcing inlining at every call site duplicates two 16-bit div routines
(~90 bytes on ESP8266 Xtensa, no fast 16-bit hw div) across every addressable
effect, range op, and transition step that reads a pixel back through the
gamma curve. Move the four channel methods and the Color wrapper to the .cpp
and drop the ALWAYS_INLINE hints on the forward direction too so the
compiler picks per call site.
When a uniform-colored addressable strip transitions from one color to
another, interpolate math-only against a cached start color instead of
reading each LED's current value back through the 8-bit stored byte.
The old algorithm used led.get_red()/etc. every step as the source for
the delta, which round-tripped through gamma uncorrect/correct and the
8-bit stored byte. At gamma 2.8, any pre-gamma value below ~27 rounds
to stored byte 0, so small early-transition steps produced stored 0 and
the next step read back 0, stalling progress until ~90% of the transition
before a single step produced a large-enough pre-gamma value to clear
the gamma threshold. Result: dark for the first 9s of a 10s fade, then
jump on in the final 1s.
Detect uniform start state in start() and take a cheap math-only lerp
path when true, so the stored byte advances through each gamma threshold
as smoothed_progress crosses it. Falls back to the existing per-LED
read-back algorithm when the buffer is non-uniform (e.g. when
transitioning out of an addressable effect).
PlatformIO's idedata may list flash images that do not exist on disk
(e.g. a tasmota tinyuf2.bin referenced by the adafruit_qtpy_esp32s3_n4r2
board). Previously the CLI passed every entry straight to esptool, which
aborted the entire flash with "No such file or directory". The dashboard
path is unaffected because it flashes the pre-merged firmware.factory.bin
produced by the post-build step, which already tolerates missing inputs.
Filter non-existent extra_flash_images with a warning so a stale or
incorrect platform-declared image no longer breaks esphome run.
Fixes https://github.com/esphome/esphome/issues/15634
Addresses Copilot review suggestion on #15720: when the handler aborts a
previously interrupted OTA session because a new upload arrived, also fire
the OTA_ABORT state notification so user-facing on_abort: automations and
other OTA state listeners observe the teardown. Without this, the abort
would be silent to listeners (distinct from OTA_ERROR, which signals a
failed session rather than a superseded one).
The previous fix tracked the AsyncWebServerRequest pointer to distinguish
web_server_idf's double index==0 callbacks (Start marker with data==nullptr,
then the first real data chunk) from a retry after an interrupted upload.
That is unreliable: AsyncWebServerRequest is a stack-allocated object in the
httpd task, so a new request that happens to reuse the same stack address
as an interrupted one compares equal and silently skips the abort. Closing
a browser tab mid-upload and starting a fresh upload from another window
could then concatenate partial data from the first upload with the new
image.
Gate the init block on 'index == 0 && len > 0' instead. This uniquely
identifies the first real byte of an upload on both IDF (start-marker has
len==0) and Arduino, no identity tracking needed.
web_server_idf invokes handleUpload twice with index==0 at the start of every
upload (once as a 'Start' signal with nullptr data, then with the first data
chunk). The previous fix treated the second call as a stale-session retry,
wastefully aborting and re-initializing the backend on every single upload.
Store the AsyncWebServerRequest pointer that owns the current session and only
tear down the backend when a different request arrives at index==0. Clear the
tracked pointer on every backend reset path so a stack-reused request pointer
from a later upload cannot collide.
If a captive_portal or web_server OTA upload is interrupted mid-stream (e.g. TCP
connection reset), the shared OTARequestHandler's ota_backend_ is left open. When
the browser resends the multipart POST, the guard 'index == 0 && !ota_backend_'
skipped re-initialization, so new bytes were written at the previous session's
offset. The Updater's end() then reports success with a concatenated image in
flash, bricking the device on reboot.
Abort and reset any in-progress backend whenever a new multipart upload starts
(index == 0) so the fresh upload begins a clean OTA session.
The esp32 to_code runs at PLATFORM priority (1000) before component
to_code at COMPONENT priority (0), so the flag was set too late.
Move to config validation to ensure it is set before esp32 processes
sdkconfig options.
When flash cache is disabled during background flash operations (NVS
writes by WiFi, BLE, Zigbee, Thread, power management, etc.), the ADC
oneshot read function will crash if it is in flash. This places the
ADC oneshot control functions in IRAM by setting
CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM when the ADC component is used.
Adds require_adc_oneshot_iram() helper and adc_oneshot_in_iram advanced
config option.