libc++ eagerly instantiates the unique_ptr<APIConnection> destructor
when std::array<std::unique_ptr<APIConnection>, N> is parsed, requiring
sizeof(APIConnection). api_server.h only forward-declares APIConnection
(via list_entities.h), so the destructor instantiation fails for any
translation unit that includes api_server.h without also including
api_connection.h first.
Wrap the unique_ptr in a custom deleter (APIConnectionDeleter) whose
operator() is defined out-of-line in api_server.cpp where APIConnection
is complete. The default_delete<APIConnection> path is never
instantiated, so libc++'s incomplete-type assertion is avoided.
GCC/libstdc++ already deferred this instantiation, so this only affects
macOS host-platform builds (used by integration tests).
Apply the bitmask pattern from LightControlAction (#16039) to
cover::ControlAction (3 fields: stop, position, tilt) and
cover::CoverPublishAction (3 fields: position, tilt, current_operation).
Unused fields are elided via [[no_unique_address]] and skipped at
compile time in play() via if constexpr.
Codegen for cover.control: and cover.template.publish: builds the
bitmask from the YAML keys present. CONF_STATE and CONF_POSITION
both map to the same position bit (they are mutually exclusive YAML
keys for the same C++ field).
Per-instance: 16-28 B depending on which fields are set, down from
~28 B baseline.
Replace heap-allocated ContinuationAction/WhileLoopContinuation/
RepeatLoopContinuation instances with inline members, eliminating one
heap allocation per IfAction/WhileAction/RepeatAction at setup.
Each parent already needs exactly one continuation as the chain
terminator that hands control back. Heap-allocating it costs the
~16-byte object plus an ~8-byte heap header per instance, plus heap
fragmentation. Inlining moves the same 16 bytes from heap to BSS
and drops the heap header overhead.
Per-parent net change:
- BSS: +16 B (the inline continuation; +32 B for IfAction<true>)
- Heap: -24 B per heap allocation eliminated (16 B object + ~8 B header)
- Net RAM saved: ~8 B per simple parent, ~16 B for IfAction<true>
- Plus: one fewer heap allocation per parent at setup, less fragmentation
For IfAction<HasElse=false>, the else continuation is elided via
[[no_unique_address]] + an empty wrapper struct, so it costs 0 B.
Note: CI memory analysis only measures static RAM (BSS), not heap.
This change moves bytes from heap to BSS, so the report will show
BSS increasing while the actual heap savings (and fragmentation
reduction) are not directly visible.
Apply the X-macro pattern from #15132 so the field list is declared
once and expanded into setters, play(), and storage. Drop the unused
BIT_NAME and tag arguments — only (type, name, idx) is needed since
idx doubles as both the bit position and the Empty<> tag.
Net change vs upstream is +10 LOC instead of +64.
Parameterize LightControlAction on a uint16_t Fields bitmask encoding
which of its 14 templatable fields are configured. Unset fields are
elided from the instance via [[no_unique_address]] and skipped at
compile time in play() via if constexpr.
Real-world configs typically use only 1-5 of the 14 fields. Codegen
computes the bitmask from the YAML and passes it as the leading
template argument, so each unique field combination produces its own
type with only the storage and play() branches it actually needs.
Measured on apollo-pump-1-5d9bdc.yaml (7 instances, 3 unique masks):
- state-only (2 instances): 72 B -> 20 B = 52 B saved each
- state+RGB (2 instances): 72 B -> 32 B = 40 B saved each
- state+brightness+RGB (3 instances): 72 B -> 36 B = 36 B saved each
- Total: ~292 B RAM saved
- Flash cost: ~30-70 B for 3 play() variants + vtables
Parameterize DimRelativeAction on a HasTransitionLength bool, mirroring
the same trick applied to ToggleAction. When transition_length is not
configured in YAML, the TemplatableFn field is elided via
[[no_unique_address]] and LightCall::set_transition_length is skipped
via if constexpr.
Saves 4 bytes RAM per dim_relative action instance and shrinks play()
by ~48 bytes when transition_length is unused.
Parameterize ToggleAction on a HasTransitionLength bool, mirroring the
IfAction<HasElse> pattern. When transition_length is not configured in
YAML, the TemplatableFn field is elided via [[no_unique_address]] and
LightCall::set_transition_length is skipped via if constexpr.
Saves 4 bytes RAM per toggle action instance and shrinks play() from
79 to 28 bytes when transition_length is unused.
- 'all clients are idle' was accurate pre-patch; now this branch can run
while CONNECTED/ESTABLISHED clients exist. Clarify which states actually
block, and note that the inner coex revert has its own gate.
- Generalize the coex-revert rationale: 'lock's GATT Write Response' was
Yale-specific; the bug is generic to any peer GATT response that needs
to reach us while WiFi competes for the shared radio.
count_client_states_() only tracks transient states (CONNECTING, DISCOVERED,
DISCONNECTING). When a connection settles into CONNECTED/ESTABLISHED, all
counted states go to zero, and loop() reverts coex from PREFER_BT back to
BALANCE while the connection is still live. Sustained WiFi traffic then
crowds out the BT radio's ability to receive the peer's GATT response,
causing esp_ble_gattc_write_char_descr to time out at ~20s with a
host-synthesized status=0x85 (commonly reported as status=133).
Fix: count CONNECTED + ESTABLISHED clients as "active" and gate the coex
revert on the active count. Coex now stays at PREFER_BT for the entire
lifetime of any active connection and only returns to BALANCE when all
connections are torn down.
Validated end-to-end: aioesphomeapi-driven write_descriptor on a Yale
BETA211123 lock at LOG_LEVEL_VERBOSE (heavy WiFi traffic, the
bug-triggering condition) — pre-patch: 19s hang then status=133.
Post-patch: 228ms SUCCESS. Verified live on an M5 Atom Lite running as a
bluetooth_proxy with host BlueZ stopped (M5 as the only path): lock ops
succeed cleanly with zero status=133 errors. A multi-day production
soak window will be added to the PR body before merge.
The generated src/main.cpp.o was being skipped by _scan_source_symbols
(which only walked src/esphome/) and _source_file_to_component had no
rule for it. As a result setup()/loop() fell through to the
"app_framework" heuristic in const.py instead of being counted as
[esphome]core, both via the nm scan path and the linker map path.
Scan all of src/ and recognize .../src/main.cpp.o as core.
The setup_<X>_core_ functions are decorated with @setup_entity, which calls
finalize_entity_strings() before returning. queue_entity_register() must run
before that so the combined App.register_<entity>(var, name, hash, packed)
emission is selected; otherwise finalize falls back to configure_entity_ and
no register call is emitted.
Reported by Copilot review on PR #16030.