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).
Address two issues on the new data-loop timeout:
- clang-tidy ESP32 Arduino flagged "cannot jump from this goto statement to its label" at the existing pre-loop `goto error` sites because the new `uint32_t last_data_ms = millis();` declaration sat between them and the `error:` label, so the jumps would skip past its initialization. Move the declaration up next to the other handle_data_ locals (`total`, `last_progress`) and just assign it before the loop entry. The earlier gotos no longer cross an init.
- Copilot pointed out the new timeout `goto error` left `error_code` at `OTA_RESPONSE_OK` (set by `backend_->begin()`), so the device would write OK back to the uploader even though it was aborting. Set `error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN` before the goto so the uploader receives a correct failure code.
The main `while (total < ota_size)` loop in `ESPHomeOTAComponent::handle_data_()`
had a `// TODO: timeout check` and no wall-clock guard. If the uploader
side dropped the TCP connection without a FIN/RST being delivered to
the device (uploader process killed mid-transfer, NAT/router state
dropped, packet loss eating the RST), `recv_fn` would never see a
close and `s_err_fn` would never fire, so:
- `pcb_` stays non-null
- `rx_closed_` stays false
- `waiting_for_data_()` stays true forever
- the loop spins on `EWOULDBLOCK`, fed by `App.feed_wdt(); continue;`,
indefinitely
LwIP TCP keepalive is not enabled on the OTA socket, and even when
enabled the default keepalive timer is on the order of hours, so the
device is effectively unresponsive until power cycle. This matches
the "device unresponsive until I do a hard power-reset" symptom in
issue #15953.
Track the timestamp of the last successful read and abort if no data
arrives for `OTA_SOCKET_TIMEOUT_DATA` (90s, same constant the handshake,
`readall_()`, and `writeall_()` already use). The timeout resets on
every successful read so a slow but live link does not false-trigger.
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.
When `g_main_loop_woke` is already set on entry to `wakeable_delay()`,
both the ESP8266 and RP2040 paths consume the flag and return without
yielding. That's safe in isolation, but if a caller loops on
`wakeable_delay()` (e.g. `LWIPRawImpl::wait_for_data_()` waiting for
SO_RCVTIMEO), and ISR sources (GPIO, timer, WiFi RX on ESP8266; alarm
/ async on RP2040) keep re-setting the flag between iterations, every
iteration takes the fast path and the loop never yields.
That can starve the SDK / async context, blocking actual TCP delivery
to our socket and causing OTA reads to time out on busy devices even
though there is data in flight. The PR that introduced
`wait_for_data_()` (#14675) relied on `wakeable_delay()` to yield on
every iteration; this restores that property on the fast path.
Adds `delay(0)` on ESP8266 and `yield()` on RP2040 to the fast path,
matching the yield behaviour those platforms already use for the
`ms == 0` poll case.
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.