Commit Graph
27258 Commits
Author SHA1 Message Date
J. Nick Koston 2cd54b209d Merge branch 'fast-millis-esp8266' into integration 2026-04-12 09:37:02 -10:00
J. Nick Koston a8cb5b44cd [esp8266] Address review: fix misleading comment, add named constants
- Remove 'No __umulsidi3' claim from top comment — the rare path
  does use it via /1000. Reworded to 'pure 32-bit ops on the common
  path'.
- Extract MILLIS_RARE_PATH_THRESHOLD_US and US_PER_MS constants to
  replace magic numbers 10000 and 1000.
2026-04-12 00:03:23 -10:00
J. Nick Koston c863a204e8 [esp8266] Fix incorrect claim about delay() usage scope
delay() is called from component loop() and update() methods too
(NFC, RTTTL, sensors), not just setup/OTA.
2026-04-11 23:36:40 -10:00
J. Nick Koston ebbc3b57c7 [esp8266] Document delay() semantic difference from Arduino
Arduino's delay() uses esp_suspend() with a one-shot os_timer for
efficient single-suspension waiting. Our replacement polls millis()
with optimistic_yield(), which also enters esp_schedule/esp_suspend
via yield() but resumes repeatedly. Functionally correct for ESPHome
(delay is cold path, SDK tasks run via yield), just less power-
efficient for long delays.
2026-04-11 23:34:14 -10:00
J. Nick Koston b882d8c721 [esp8266] Fix stale s_last_us reference in comment
Updated to state.last_us to match the struct refactor.
2026-04-11 23:31:48 -10:00
J. Nick Koston da9c44d28d [esp8266] Bound the critical section in millis() accumulator
Split the μs→ms conversion into two paths to keep the interrupt-
disabled critical section bounded:

- Common path (delta < 10 ms): while loop runs at most 10 iterations
  (~100 ns). This covers the normal hot-path case where millis() is
  called thousands of times per second.

- Rare path (delta >= 10 ms): constant-time multiply-by-reciprocal
  via /1000 (compiled to __umulsidi3, ~2.5 μs). Only fires after a
  long block (WiFi scan, boot, component stall), where the extra
  latency is negligible relative to the block that caused it.

Addresses the concern that a multi-second WiFi scan block could cause
the unbounded while loop to hold interrupts for tens of microseconds.
2026-04-11 23:21:15 -10:00
J. Nick Koston aec0be356f [esp8266] Remove unused USE_FAST_MILLIS_ACCUMULATOR define 2026-04-11 23:16:06 -10:00
J. Nick Koston 3d3e26d141 [esp8266] Merge millis_accumulator into millis(), use struct for statics
Merge the separate millis_accumulator() function directly into millis()
to eliminate the extra function call overhead and prologue/epilogue.
Pack the three statics into a struct so the compiler loads one base
address instead of three literal pool entries.

Saves ~12 bytes of IRAM (87+6 → 81 bytes).
2026-04-11 23:10:19 -10:00
J. Nick Koston bf20403558 [esp8266] Revert init() wrap — micros64() needs the overflow timer
Arduino's micros64() reads the same overflow tracking statics that
init() sets up. Wrapping init() as a no-op would cause micros64()
to silently return wrong values after 71 minutes. The overflow timer
cost is negligible (~3 μs per 60 seconds).
2026-04-11 23:08:53 -10:00
J. Nick Koston dff7757d1e [esp8266] Suppress clang-tidy for __wrap_millis reserved identifier
The __wrap_ prefix is required by the GNU linker's --wrap feature.
It's a reserved identifier by C++ rules but mandated by the toolchain.
2026-04-11 23:02:16 -10:00
J. Nick Koston cb654a1e50 [esp8266] Handle delay(0) as a yield point
Arduino's delay(0) calls yield() once — used as a yield point by some
code. Our millis-based loop would skip yielding entirely since
0 < 0 is false. Add explicit delay(0) → yield() handling.
2026-04-11 22:59:12 -10:00
J. Nick Koston 325d9db499 [esp8266] Replace delay() with yield loop to allow GC of original millis
Arduino's ::delay() is implemented in core_esp8266_wiring.cpp alongside
millis(). Because __delay calls millis() within the same compilation
unit, --wrap=millis can't intercept that intra-object reference, which
prevents the linker from garbage-collecting the original millis body
(~80 bytes of IRAM).

Replace esphome::delay() with a simple yield loop that uses our fast
millis() accumulator. This has the same behavior as Arduino's delay:
feeds the watchdog and processes SDK tasks via yield(), keeping WiFi
alive. With __delay unreferenced, the linker can now GC both __delay
and the original millis function.
2026-04-11 22:56:59 -10:00
J. Nick Koston dff6199fc8 [esp8266] Add USE_FAST_MILLIS_ACCUMULATOR define for benchmark guard 2026-04-11 22:37:45 -10:00
J. Nick Koston 16f7effcf3 [esp8266] Fix clang-format: drop trailing underscore from static function 2026-04-11 22:21:57 -10:00
J. Nick Koston 406546876a [esp8266] Replace millis() with fast accumulator, wrap Arduino callers
Arduino ESP8266's millis() uses 4x 64-bit multiplies with magic
constants to convert system_get_time() to ms while tracking overflow.
On the LX106 (no hardware multiply-high instruction), each 64-bit
multiply goes through the __umulsidi3 software helper — costing
~3.3 us per call.

Replace with a simple accumulator that tracks a running millis counter
from system_get_time() deltas using pure 32-bit integer ops (subtract,
add, compare, subtract). No 64-bit math, no __umulsidi3.

Use -Wl,--wrap=millis to intercept all ::millis() calls site-wide so
Arduino libraries and ISR handlers (Wiegand, ZyAura) also get the fast
version. Brief interrupt disable (~125 ns) protects the static state.

Overflow safety: unsigned 32-bit delta arithmetic handles the 71-minute
system_get_time() wrap correctly for one wrap. ESPHome calls millis()
thousands of times per second, so missing a full wrap is not a realistic
concern. At boot, both the accumulator and system_get_time() start at 0,
so no special initialization is needed.

Benchmarked on real ESP8266 hardware:
  Before: 3348 ns/call (Arduino 4x 64-bit multiply)
  After:  ~800-900 ns/call (accumulator, estimated)
2026-04-11 22:16:38 -10:00
J. Nick Koston 8800c10181 [core] Address review: fix docstrings per Copilot feedback
- get_app_state(): say 'STATUS_LED_* only', not 'STATUS_LED_* and lifecycle'
  since lifecycle bits are no longer maintained in app_state_.
- STATUS_LED_SETTLE_S: remove incorrect mention of feed_wdt re-dispatch;
  status_led_light is driven by the main loop, not feed_wdt.
- snapshot_led service: say 'status_led_light output' not 'pin state'
  since the fixture uses a template output, not GPIO.
2026-04-11 17:37:59 -10:00
J. Nick Koston 79f27d7a19 Merge branch 'drop-redundant-app-state-or' into integration 2026-04-11 16:39:14 -10:00
J. Nick Koston 515ec544ce [tests] Extract STATUS_LED_SETTLE_S constant, fix idle count check
Extract the 0.3s magic sleep into a named constant explaining why that
duration is chosen (feed_wdt re-dispatches every ~3 ms; 300 ms gives
~100 opportunities). Fix the idle-write check to snapshot AFTER the
clear instead of before it, so writes still in-flight from the error
phase don't inflate the delta.
2026-04-11 16:32:15 -10:00
J. Nick Koston 542b1fc7f3 [tests] Add set→clear→re-set round-trip check for status_led
Verifies that after clearing all status flags, re-setting a flag makes
status_led_light resume writing to its output. Guards against a future
idle optimization (like #15642) where status_led disables its own
loop() when idle: if the re-enable path were broken, the second set
would not produce writes.

Also checks that writes STOP after all flags are cleared (counter
should not keep growing), proving status_led_light correctly stops
blinking in steady state.
2026-04-11 16:28:54 -10:00
J. Nick Koston 1b1adb841a [core] Fix clang-format: remove extra blank line 2026-04-11 16:21:43 -10:00
J. Nick Koston 424f1c7b0a [core] Rename any_component_has_status_flag and add integration tests
Drop the trailing underscore from any_component_has_status_flag now
that the method is public. Trailing underscores in the codebase are
reserved for protected/private members per clang-tidy naming rules,
which caused a CI failure on the previously-named public helper.

Add integration tests covering:
- Single-component status_set/clear for warning and error
- Multi-component OR semantics (both clear orders)
- Warning and error independence
- End-to-end proof that status_led_light::loop() reads App.app_state_
  and writes its output when the bits are set (via a fake template
  output whose write_action bumps a counter exposed as a sensor)
2026-04-11 16:20:05 -10:00
J. Nick Koston d9c6b27cf2 [core] Drop per-iteration app_state_ accumulation from main loop
Application::loop() used to, on every iteration of the inner component
loop, OR each component's state into a local accumulator and then OR
that into this->app_state_, finally overwriting this->app_state_ with
the accumulator after the loop. That was ~5 instructions per component
per loop iteration on the hot path, paying to rebuild state that's
already kept current elsewhere.

STATUS_LED_WARNING / STATUS_LED_ERROR are the only app_state_ bits
anything reads. Component::set_status_flag_() already writes them to
App.app_state_ directly the moment they're set, so any reader (status_led
components, feed_wdt's LED re-dispatch) already sees them without the
per-iter OR. The per-iter OR only ever re-set bits that were already
there — pure dead code on the hot path.

The clear path is handled by moving the work into
status_clear_warning/error_slow_path_(): on a real set->clear transition
the helpers walk App.components_ once to check if any other component
still has the flag, and clear App.app_state_ if not. Clears are rare
relative to loop iterations (a logged transition event), so O(N) per
clear is far cheaper than O(N) per loop.

Setup subtlety: Application::setup()'s slow-setup busy-wait forces
STATUS_LED_WARNING on to blink the LED while a slow component initializes.
A component clear during setup would prematurely wipe that forced bit,
so status_clear_warning_slow_path_() gates its walk-and-clear behind
APP_STATE_SETUP_COMPLETE (a free bit on app_state_ — zero RAM cost).
Application::setup() reconciles the warning state once at the end and
sets the flag. STATUS_LED_ERROR is never forced so its clear path
always walks.

Also fixes a pre-existing bug: the old per-iter accumulation only iterated
looping_components_, so non-looping components' status bits would get
clobbered by the end-of-loop 'app_state_ = new_app_state' overwrite.
The walk-on-clear approach iterates components_ (all of them), so
non-looping components are respected.
2026-04-11 16:05:46 -10:00
J. Nick Koston 57592fa664 Merge remote-tracking branch 'upstream/feed-wdt-inline-fastpath' into integration 2026-04-11 15:22:33 -10:00
J. Nick Koston 5926ca5369 [core] Split feed_wdt into hot and cold entries
Separate Application::feed_wdt() into two entry points so the hot path
callers stop paying for the time==0 check they never trigger:

- feed_wdt_with_time(time): inline, hot path. Rate-limit check in 3
  Xtensa instructions (load + sub + branch). [[unlikely]] tells the
  compiler the slow branch is rare so the common path stays
  fall-through.
- feed_wdt(): cold, out of line. Fetches millis() and forwards through
  the same rate limit. Used by setup loops, upload helpers, yield(),
  and any other non-hot caller.

feed_wdt_slow_() is now pure worker code — 11 bytes. It just calls
arch_feed_wdt(), updates last_wdt_feed_, and runs the status LED
re-dispatch. Both entries have already confirmed the rate limit was
exceeded before calling.

Hot call sites updated:
- Application::loop() per-component feed
- Scheduler::execute_item_() after each scheduled item runs
- Application::teardown_components() inner loop (already has 'now')
2026-04-11 15:19:35 -10:00
J. Nick Koston 1815398062 Merge remote-tracking branch 'upstream/feed-wdt-inline-fastpath' into integration 2026-04-11 15:05:52 -10:00
J. Nick Koston dc5626eb85 [core] Invert feed_wdt condition so slow-path call is inside the if
Cleaner than the early-return form — the action (calling feed_wdt_slow_)
reads as the body of the conditional instead of falling through past a
guard clause. Logically identical and compiles to the same code.
2026-04-11 14:57:01 -10:00
J. Nick Koston 715f0ca6f7 [core] Extract WDT_FEED_INTERVAL_MS constexpr
Replace the magic 3 in both the inline feed_wdt check and the slow path
with a named constexpr so the rate-limit threshold is defined once.
2026-04-11 14:54:02 -10:00
J. Nick Koston a70ec9ec06 [scheduler] Feed watchdog after each scheduled item, drop top-of-loop feed
The main loop used to feed the watchdog unconditionally right after
Scheduler::call() returned, regardless of whether the scheduler had any
actual work to do. On an idle device this meant every outer loop
iteration paid the inline rate-limit check (load + sub + branch) for no
benefit.

Move the feed into Scheduler::execute_item_() so it fires only after a
scheduled callback actually runs, and covers both the main heap path
and the defer queue path (both go through execute_item_). This also
bounds the max feed gap during a burst of back-to-back scheduled items
by max(item_runtime) instead of sum(item_runtime).

The top-of-loop feed in Application::before_loop_tasks_() is now
unnecessary — when Scheduler::call does no work, the only elapsed time
is the sleep wake plus a few instructions, and when it does have work,
it fed the wdt as it went.
2026-04-11 14:51:59 -10:00
J. Nick Koston ddbf6f2347 [core] Inline feed_wdt hot path with out-of-line slow path
Split Application::feed_wdt() into an ALWAYS_INLINE wrapper that checks
the 3ms rate limit against last_wdt_feed_ and a feed_wdt_slow_() callee
that performs the actual arch_feed_wdt() + status LED re-dispatch.

Callers on the hot path (loop_task before/after each component) that
already have a millis() timestamp in hand now pay only a load + sub +
branch on the no-op path instead of a full call8 / entry / retw.

Moves the rate-limit state from a function-local static to a class
member (last_wdt_feed_) so the inline can access it.
2026-04-11 14:32:49 -10:00
Jonathan Swoboda bef4c8a86c [cc1101] Extract chip configuration into configure() method (#15635) 2026-04-11 17:36:27 -04:00
J. Nick Koston ea0ba99c28 [core] Drop stale register_socket defs after ota merge
PR #15639 removed Application::register_socket / monitored_sockets_
on the fast-select path before the ota-disable-loop-when-idle merge.
The merge re-introduced the function definitions without the matching
declarations. Drop them.
2026-04-11 08:40:17 -10:00
J. Nick Koston a6523fde47 Merge remote-tracking branch 'upstream/ota-disable-loop-when-idle' into integration
# Conflicts:
#	esphome/core/application.cpp
2026-04-11 08:36:24 -10:00
J. Nick Koston bfcfd7f110 Merge branch 'dev' into ota-disable-loop-when-idle 2026-04-11 08:28:06 -10:00
J. Nick Koston 30de0c17ab [esphome.ota] Trim verbose comments from simplification 2026-04-11 08:21:11 -10:00
J. Nick Koston e67fac704c [esphome.ota] Move OTA wake pointer out of Application into ota_esphome.cpp
Following the zephyr_mcumgr precedent, keep the single-instance pointer
as a file-scope static inside ota_esphome.cpp instead of plumbing an
ota_wake_component_ slot and setter through Application. The extern "C"
wake trampoline also lives in the same TU now, so nothing in core/
touches OTA-specific state.

Guard the C and C++ call sites on USE_OTA_PLATFORM_ESPHOME (added as a
-D flag from components/esphome/ota/__init__.py) instead of the broader
ESPHOME_USE_OTA that base ota/ used to emit — the trampoline symbol only
exists when the esphome OTA platform is actually compiled in.

Also drop the dead host yield_with_select_ wake hook (host has no OTA
platform today).
2026-04-11 08:17:49 -10:00
Farmer-shin 6e67864510 [epaper_spi] Add Waveshare 3.97inch E-Paper Display (#15466) 2026-04-11 21:27:25 +10:00
J. Nick Koston a977f99bb1 Merge remote-tracking branch 'upstream-ssh/status-led-disable-loop-idle' into integration 2026-04-11 00:16:56 -10:00
J. Nick Koston 7eb56e1cf6 [status_led] Clarify feed_wdt LOOP_DONE comment 2026-04-11 00:14:40 -10:00
J. Nick Koston 56029457f4 [status_led] Restructure feed_wdt to tail-call status_led loop() 2026-04-11 00:13:05 -10:00
J. Nick Koston f6311c3846 [status_led] Use millis() to survive blocking waits via feed_wdt 2026-04-11 00:07:22 -10:00
J. Nick Koston fcd1aa5a40 [status_led] Hoist state read and dispatch loop() directly from feed_wdt 2026-04-11 00:05:57 -10:00
J. Nick Koston 0f80c16507 [status_led] Skip feed_wdt dispatch entirely when idle 2026-04-11 00:03:55 -10:00
J. Nick Koston c9a68baa69 [status_led] Remove unrelated changes from application.cpp 2026-04-10 23:58:36 -10:00
J. Nick Koston af7fdc9887 [status_led] Simplify loop body, rely on disable_loop for idle optimization 2026-04-10 23:49:56 -10:00
J. Nick Koston 59d0f9dcbd [status_led] Disable loop when idle and switch blink timing to deadline-based 2026-04-10 23:44:35 -10:00
dependabot[bot] c2af4874f9 Bump aioesphomeapi from 44.13.2 to 44.13.3 (#15641)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 08:58:20 +00:00
dependabot[bot] 2001b91280 Bump resvg-py from 0.3.0 to 0.3.1 (#15640)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 08:57:39 +00:00
J. Nick Koston a76ed9cc77 Merge remote-tracking branch 'upstream/dependabot/pip/aioesphomeapi-44.13.3' into integration 2026-04-10 22:56:04 -10:00
dependabot[bot] f3357f47c6 Bump aioesphomeapi from 44.13.2 to 44.13.3
Bumps [aioesphomeapi](https://github.com/esphome/aioesphomeapi) from 44.13.2 to 44.13.3.
- [Release notes](https://github.com/esphome/aioesphomeapi/releases)
- [Commits](https://github.com/esphome/aioesphomeapi/compare/v44.13.2...v44.13.3)

---
updated-dependencies:
- dependency-name: aioesphomeapi
  dependency-version: 44.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 08:53:16 +00:00
J. Nick Koston d30d8f141f Merge remote-tracking branch 'upstream/core-remove-fast-select-pre-sleep-scan' into integration 2026-04-10 21:35:09 -10:00