Commit Graph
27405 Commits
Author SHA1 Message Date
J. Nick Koston bd97a3ef85 Merge remote-tracking branch 'origin/light-gamma-uncorrect-outline' into integration 2026-04-13 17:03:36 -10:00
J. Nick Koston 1ee28e210a Merge remote-tracking branch 'upstream-ssh/light-validate-clamp-loop' into integration 2026-04-13 17:03:20 -10:00
J. Nick Koston c9b6253d24 [light] Fix stale comments on float_out_of_unit_range / clamp_unit_float 2026-04-13 17:01:11 -10:00
J. Nick Koston 674fb1d3f6 [light] Drop redundant is_standard_layout asserts (offsetof no longer used)
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.
2026-04-13 16:57:58 -10:00
J. Nick Koston 58885c2090 [light] Rename log_value_out_of_range_ to drop trailing underscore (clang-tidy) 2026-04-13 16:55:25 -10:00
J. Nick Koston 6491072a77 [light] Alias clamp fields via anonymous-union float[8] to eliminate pointer UB
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.
2026-04-13 16:54:43 -10:00
J. Nick Koston 8ef1057674 [light] Treat -0.0f as in range in float_out_of_unit_range
-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.
2026-04-13 16:48:00 -10:00
J. Nick Koston bdd1c413de [light] Eliminate static-local guard variables by hoisting names to PROGMEM_STRING_TABLE
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)
2026-04-13 16:38:44 -10:00
J. Nick Koston de44b8e859 [light] Tighten comments in validate_ clamp loop and LightColorValues helpers 2026-04-13 16:31:50 -10:00
J. Nick Koston 8eb8b3dc0f [light] Promote bit-pattern clamp to LightColorValues setters and add layout asserts
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)
2026-04-13 16:27:09 -10:00
J. Nick Koston 36881166a8 [light] Replace soft-float range check with union bit-cast + unsigned compare
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
2026-04-13 16:16:44 -10:00
J. Nick Koston cdde0abec7 [light] Refine validate_ clamp loop: ctz iteration, per-field asserts, typed pointers
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).
2026-04-13 16:07:34 -10:00
J. Nick Koston edb2145aca [light] Collapse 8 clamp-and-copy blocks in LightCall::validate_ into a loop
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.
2026-04-13 15:55:50 -10:00
J. Nick Koston 76c8eeede9 [light] Fix uint16_t overflow in color_uncorrect_channel_
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.
2026-04-13 15:08:36 -10:00
J. Nick Koston 10412ac2c5 Merge remote-tracking branch 'origin/light-gamma-uncorrect-outline' into integration 2026-04-13 15:04:11 -10:00
J. Nick Koston 4696d70b8a Merge remote-tracking branch 'origin/light-addressable-gamma-transition-stall' into integration 2026-04-13 15:04:07 -10:00
J. Nick Koston 12b55f176f [light] Clearer uniformity scan + note edge case in uniform path 2026-04-13 14:56:48 -10:00
J. Nick Koston b324630f8e [light] Type-annotate to_code in mock_addressable_light 2026-04-13 14:56:02 -10:00
J. Nick Koston 8da24fd1d9 [light] Use existing integration test helpers in transition test 2026-04-13 14:54:39 -10:00
J. Nick Koston 32130e1cb1 [light] Address Copilot review feedback on PR #15726
- 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
2026-04-13 14:53:17 -10:00
J. Nick Koston 827afb0e98 [light] Move gamma uncorrect math out-of-line, drop ALWAYS_INLINE hints
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.
2026-04-13 14:47:11 -10:00
J. Nick Koston 93893e02a7 Revert: raw-byte uniformity check (didn't help inlining) 2026-04-13 14:35:23 -10:00
J. Nick Koston 6edadaa33b [light] Use raw byte compare for uniformity scan to keep apply() hot path inlinable 2026-04-13 14:31:42 -10:00
J. Nick Koston 4e8f98e767 [light] Collapse uniform-start flag+Color into optional<Color> 2026-04-13 14:22:13 -10:00
J. Nick Koston 3f56e0255a [light] Avoid addressable transition stall at low gamma-corrected values
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).
2026-04-13 14:17:21 -10:00
J. Nick Koston 21df5d9bf6 [web_server] Reset OTA backend on new upload to avoid brick after interrupted OTA (#15720) 2026-04-13 13:59:45 -10:00
J. Nick Koston 73c972a604 [adc] Place ADC oneshot control functions in IRAM for cache safety (#15717) 2026-04-13 13:59:32 -10:00
J. Nick Koston 4dfdcfc46d Merge remote-tracking branch 'upstream/adc-iram-safety' into integration 2026-04-13 13:51:21 -10:00
J. Nick Koston ddf5ab6d1c Merge remote-tracking branch 'upstream/esptool-skip-missing-flash-images' into integration 2026-04-13 13:51:03 -10:00
J. Nick Koston 5f76b78cfa [esphome] Skip missing extra flash images in upload_using_esptool
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
2026-04-13 13:37:29 -10:00
J. Nick Koston 48a611b625 Merge remote-tracking branch 'upstream/captive-portal-ota-resume-brick' into integration 2026-04-13 13:21:31 -10:00
J. Nick Koston c26d180325 Emit OTA_ABORT state when tearing down a stale session on retry
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).
2026-04-13 13:07:50 -10:00
J. Nick Koston ba2558edf6 Gate OTA session init on index==0 && len>0 instead of request pointer
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.
2026-04-13 12:56:52 -10:00
J. Nick Koston 3042a910fe Extract ota_end_session_ helper to DRY backend reset paths 2026-04-13 12:49:44 -10:00
J. Nick Koston ccdb5a3bfb Track request pointer to avoid re-init on web_server_idf's double index==0 call
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.
2026-04-13 12:49:11 -10:00
J. Nick Koston 81a2caee36 [web_server] Reset OTA backend on new upload to avoid bricked device after interrupted OTA
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.
2026-04-13 12:41:12 -10:00
J. Nick Koston 70ee6a5031 Add type annotations to _require_adc_iram 2026-04-13 12:28:09 -10:00
J. Nick Koston 8c0436cc7e [adc] Move require_adc_oneshot_iram to config validation
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.
2026-04-13 12:25:38 -10:00
Jonathan Swoboda 8cdffef82a [heatpumpir] Bump tonia/HeatpumpIR to 1.0.41 (#15711) 2026-04-13 17:06:56 -04:00
J. Nick Koston d1d96d488d [adc] Place ADC oneshot control functions in IRAM for cache safety
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.
2026-04-13 11:04:29 -10:00
dependabot[bot] 4034809281 Bump actions/create-github-app-token from 3.0.0 to 3.1.1 (#15712)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 11:00:46 -10:00
dependabot[bot] ce6bffb65c Bump actions/cache from 5.0.4 to 5.0.5 (#15713)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 11:00:24 -10:00
dependabot[bot] e8bc4bedb4 Bump actions/cache from 5.0.4 to 5.0.5 in /.github/actions/restore-python (#15714)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 11:00:11 -10:00
J. Nick Koston b85a7ef317 [scheduler] Force-inline process_to_add() fast path (#15685) 2026-04-13 08:40:58 -10:00
J. Nick Koston 9f7e310526 [scheduler] Force-inline cleanup_() fast path (#15683) 2026-04-13 08:40:39 -10:00
J. Nick Koston af7cb1d81e [scheduler] Force-inline process_defer_queue_() fast path (#15686) 2026-04-13 08:40:25 -10:00
J. Nick Kostonandpre-commit-ci-lite[bot] 53ce2a2f7f [api] Add speed_optimized to SubscribeLogsResponse (#15698)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-04-14 06:25:05 +12:00
Jonathan Swoboda fb0283e0ee [esp32] Update the recommended platform to 55.03.38-1 (#15705) 2026-04-13 14:18:52 -04:00
Jonathan Swoboda 5d0cfc31fa [core] Move FILTER_PLATFORMIO_LINES into platformio_runner (#15707) 2026-04-13 14:18:44 -04:00
J. Nick Koston f30f0a0edc [zephyr] Remove redundant yield() from main loop (#15694) 2026-04-13 09:43:17 -04:00