esp_now_send built the outbound frame into a function-static buffer
before request() took g_req_mutex, so two tasks calling the public
esp_now_send symbol could overwrite each other's frame. It also meant
every transmit copied the body twice: once into that buffer, then again
into request()'s own buffer.
request() now takes an optional tail chunk that it writes straight after
the payload, under the mutex. esp_now_send builds only the 9-byte header
on the stack and hands the caller's frame over as the tail. That removes
the race, drops a full-frame copy per transmit, and frees the 1479-byte
static buffer.
The wire format is unchanged: the same header bytes followed by the same
frame bytes, with the same payload_len.
ESPHome's espnow component drives esp_now_* from the main loop; on the P4 shim
each call is a blocking CustomRpc round-trip, so under co-processor load mesh
traffic stalls the loop (and the UI). Take the round-trips out of the hot path:
- Mirror the co-processor peer table locally (spinlock-guarded, since the
esp_now_* symbols are public and not guaranteed to be called only from the
main loop) so esp_now_is_peer_exist() answers with no round-trip — the espnow
component calls it twice per received frame and once per send.
- Make esp_now_send() fire-and-forget: the frame is handed to the transport and
the real TX result still arrives via the async SEND event, matching native
esp_now_send semantics (which already report completion via the callback).
- Make esp_now_add_peer()/del_peer() fire-and-forget too, updating the mirror
locally. Safe against a following send to a just-added peer: both ride the
same in-order CustomRpc channel and the co-processor processes REQs FIFO, so
ADD_PEER lands before the SEND. mod_peer stays synchronous (off the hot path).
- on_recv/on_send read the recv/send callback pointer once into a local; a
concurrent esp_now_unregister/deinit on the main loop can no longer null it
between the guard and the call (a runtime-reachable null-deref via disable()).
- on_resp fails a truncated RESP with ESP_ERR_INVALID_RESPONSE instead of
returning the co-processor status with a zeroed payload, and logs the
oversized-ret_len clamp (a wire-format-drift signal).
- register_recv_cb/register_send_cb confirm ensure_setup() succeeded before
arming the callback, so a failed setup leaves the pointer null.
- on_resp(): a truncated RESP that can't hold its claimed ret_len no longer
reports those (stale) buffer bytes as a valid return payload; it now returns
zero return bytes, matching on_recv's fail-closed behaviour.
- on_resp/on_recv/on_send: log malformed/too-short frames (WARN) and stale
post-timeout responses (VERBOSE) so wire-format drift between host and
co-processor is observable instead of surfacing only as opaque timeouts.
- esp_now_is_peer_exist(): log a warning when the RPC itself fails, so a
transport error is distinguishable from a genuinely absent peer (the native
bool signature still forces both to return false).
The header is shared verbatim with the C co-processor firmware, so its types
must use C's `typedef struct {...} name;` idiom and a C `<stdint.h>` include,
neither of which clang-tidy's C++ modernize checks accept. Wrap the struct
block in NOLINTBEGIN/END(modernize-use-using) and select <cstdint> vs
<stdint.h> on __cplusplus so both the C++ host build and the C firmware build
stay clean.
They are written from the main loop and read from the esp-hosted RX thread;
volatile matches the treatment of the g_resp_* globals and makes the
cross-thread visibility intent explicit (per review).
- ensure_setup(): gate on a dedicated g_setup_done flag set only after all
allocations and callback registrations succeed, so a partial failure can't
make a later call believe setup completed; semaphore creation is guarded so
a retry doesn't leak handles.
- esp_now_send(): reject (data=nullptr, len>0) with ESP_ERR_ESPNOW_ARG instead
of dereferencing null, matching native semantics.
- esp_now_set_wake_window(): return ESP_ERR_NOT_SUPPORTED rather than silently
claiming success for an unforwarded power-save setting.
ESP-NOW rides the Wi-Fi PHY, so on a radio-less esp32 variant the espnow
component would otherwise fail with an inscrutable "undefined reference to
esp_now_*" at link time. Fail fast in final validation instead: the P4 must
have the esp32_hosted shim, and other radio-less variants have no ESP-NOW
path at all. Adds a P4 esp32_hosted + espnow compile test and unit tests for
the new validation.
esp-hosted proxies esp_wifi.h but not esp_now.h, and esp_wifi_remote
injects the esp_now.h header on the P4 host with no implementation, so
the esp_now_* symbols are undefined at link. On a P4 host this defines
them and forwards each call to the co-processor over esp-hosted's
CustomRpc channel, letting the espnow component link and run unchanged.
Convert the i2c include to a named dict-style package key so CI can group
this component's build with others sharing the same bus, instead of flagging
it as needing migration.