ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
esp_now_hosted.cpp
Go to the documentation of this file.
1/*
2 * esp_now_hosted — host-side shim implementing <esp_now.h> over esp-hosted
3 * CustomRpc, so ESPHome's `espnow` component can run on a radio-less host
4 * (e.g. the ESP32-P4) whose radio lives on an esp-hosted co-processor.
5 *
6 * A radio-less host has no native ESP-NOW. esp_wifi_remote INJECTS the full
7 * esp_now.h header (types + declarations) but ships NO implementation, so every
8 * esp_now_* symbol is an undefined reference at link time. This translation
9 * unit provides those definitions; each forwards to the co-processor over
10 * CustomRpc (see esphome/esp-hosted-firmware for the matching coprocessor
11 * handlers). No esp-hosted or esp_wifi_remote source is patched, and there is no
12 * duplicate-symbol clash because nothing else defines these symbols here.
13 *
14 * See esp_now_hosted_rpc.h for the wire protocol.
15 */
16
17#include "sdkconfig.h"
18
19// Only build the shim on the radio-less host. On chips with a native ESP-NOW
20// stack (S3, C6, …) the real symbols exist and this file must stay empty to
21// avoid duplicate definitions.
22#if defined(CONFIG_IDF_TARGET_ESP32P4)
23
24#include <cstring>
25
26#include "freertos/FreeRTOS.h"
27#include "freertos/semphr.h"
28
29#include "esp_idf_version.h"
30#include "esp_log.h"
31#include "esp_timer.h"
32
33#include <esp_now.h> // injected declarations we are now DEFINING
34#include <esp_wifi_types.h> // wifi_pkt_rx_ctrl_t, wifi_tx_info_t
35
36// esp_hosted_misc.h (host) ships WITHOUT an extern "C" guard, so including it
37// from C++ would give its declarations C++ linkage and the real C symbols in
38// libesp_hosted would go unresolved at link. Wrap it. (Verified vs
39// esp_hosted 2.12.9.)
40extern "C" {
41#include "esp_hosted_misc.h" // esp_hosted_{send_custom_data,register_custom_callback}
42}
43
44#include "esp_now_hosted_rpc.h"
45#include "esphome/core/log.h"
46
47namespace {
48
49ESPHOME_LOG_TAG(TAG, "esp_now_hosted");
50
51// One outstanding request at a time. ESPHome drives esp_now_* from the main
52// loop; the matching response and the async RECV/SEND events all arrive on the
53// single esp-hosted RPC RX thread. Serializing requests keeps the shared
54// response slot race-free; a sequence number stops a late/stale response from
55// being mistaken for ours.
56// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables): the shim is
57// C-ABI state shared between the main loop and the esp-hosted RX thread.
58SemaphoreHandle_t g_req_mutex = nullptr;
59SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands
60bool g_setup_done = false; // set only after setup fully succeeds
61uint8_t g_seq = 0;
62volatile uint8_t g_expect_seq = 0;
63volatile int32_t g_resp_status = 0;
64uint8_t g_resp_ret[16];
65volatile uint16_t g_resp_ret_len = 0;
66
67// Written from the main loop (register/unregister/deinit), read from the
68// esp-hosted RX thread (on_recv/on_send). volatile for the same reason the
69// g_resp_* globals are: force the RX thread to observe an updated pointer
70// (e.g. a nulling by esp_now_deinit) rather than a cached one.
71volatile esp_now_recv_cb_t g_recv_cb = nullptr;
72volatile esp_now_send_cb_t g_send_cb = nullptr;
73
74// Local mirror of the co-processor's peer table. ESPHome's espnow component
75// calls esp_now_is_peer_exist() on the main loop for every received frame
76// (twice) and every send; forwarding each as a blocking RPC round-trip stalls
77// the loop. The shim is the only path that mutates the co-processor peer table
78// (add/del/deinit all go through here), so this mirror is authoritative and
79// esp_now_is_peer_exist() can answer from it with no round-trip.
80//
81// esp_now_* are public C symbols: any component or user lambda may call them,
82// and although ESPHome's espnow touches peers only from the main loop today
83// (its RX/TX callbacks merely enqueue), the shim cannot rely on that. A short
84// spinlock keeps the mirror consistent from any task/core, matching native
85// esp_now_*'s own internal thread-safety. The critical sections are a bounded
86// (<=20-entry) scan, so they stay tiny. ESP_NOW_MAX_TOTAL_PEER_NUM is 20.
87constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20;
88uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6];
89size_t g_peer_count = 0;
90portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED;
91// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
92
93// Caller must hold g_peer_lock.
94int peer_cache_find_locked(const uint8_t *mac) {
95 for (size_t i = 0; i < g_peer_count; i++) {
96 if (memcmp(g_peer_cache[i], mac, 6) == 0)
97 return static_cast<int>(i);
98 }
99 return -1;
100}
101
102bool peer_cache_contains(const uint8_t *mac) {
103 portENTER_CRITICAL(&g_peer_lock);
104 const bool found = peer_cache_find_locked(mac) >= 0;
105 portEXIT_CRITICAL(&g_peer_lock);
106 return found;
107}
108
109void peer_cache_add(const uint8_t *mac) {
110 portENTER_CRITICAL(&g_peer_lock);
111 if (peer_cache_find_locked(mac) < 0 && g_peer_count < ESP_NOW_HOSTED_MAX_PEERS)
112 memcpy(g_peer_cache[g_peer_count++], mac, 6);
113 portEXIT_CRITICAL(&g_peer_lock);
114}
115
116void peer_cache_remove(const uint8_t *mac) {
117 portENTER_CRITICAL(&g_peer_lock);
118 const int idx = peer_cache_find_locked(mac);
119 if (idx >= 0) {
120 g_peer_count--;
121 if (static_cast<size_t>(idx) != g_peer_count) // move the last entry into the gap
122 memcpy(g_peer_cache[idx], g_peer_cache[g_peer_count], 6);
123 }
124 portEXIT_CRITICAL(&g_peer_lock);
125}
126
127void peer_cache_clear() {
128 portENTER_CRITICAL(&g_peer_lock);
129 g_peer_count = 0;
130 portEXIT_CRITICAL(&g_peer_lock);
131}
132
133// ── CustomRpc event handlers (run on the esp-hosted RPC RX thread) ──────────
134// Keep them short and non-blocking. In particular they MUST NOT call back into
135// any esp_now_* shim function: that would try to take g_req_mutex / wait on the
136// RX thread that delivers the response, and deadlock.
137
138void on_resp(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) {
139 if (len < sizeof(esp_now_hosted_resp_t)) {
140 ESP_LOGW(TAG, "RESP too short: %u bytes", static_cast<unsigned>(len));
141 return;
142 }
143 const auto *r = reinterpret_cast<const esp_now_hosted_resp_t *>(data);
144 if (r->seq != g_expect_seq) { // late response from a timed-out request (expected)
145 ESP_LOGV(TAG, "dropping stale RESP seq %u (want %u)", r->seq, g_expect_seq);
146 return;
147 }
148 g_resp_status = r->status;
149 uint16_t rl = r->ret_len;
150 if (rl > sizeof(g_resp_ret)) {
151 // Larger than any real opcode return — a likely wire-format drift signal.
152 ESP_LOGW(TAG, "RESP ret_len %u exceeds buffer, clamping (wire drift?)", rl);
153 rl = sizeof(g_resp_ret);
154 }
155 if (len >= sizeof(esp_now_hosted_resp_t) + rl) {
156 memcpy(g_resp_ret, r->ret, rl);
157 } else {
158 // Truncated frame: fail closed. Never hand the caller stale bytes left in
159 // g_resp_ret by a previous response, and don't let request() report a
160 // zeroed payload as success — override the status to an error.
161 ESP_LOGW(TAG, "RESP truncated: claims %u ret bytes, frame too short", rl);
162 rl = 0;
163 g_resp_status = ESP_ERR_INVALID_RESPONSE;
164 }
165 g_resp_ret_len = rl;
166 xSemaphoreGive(g_resp_sem);
167}
168
169void on_recv(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) {
170 // Read the volatile pointer once: esp_now_unregister_recv_cb()/deinit() (via
171 // the espnow component's disable()) can null it on the main loop between the
172 // guard and the call, which would otherwise turn the call into a null-deref.
173 const esp_now_recv_cb_t cb = g_recv_cb;
174 if (cb == nullptr)
175 return;
176 if (len < sizeof(esp_now_hosted_recv_evt_t)) {
177 ESP_LOGW(TAG, "RECV too short: %u bytes", static_cast<unsigned>(len));
178 return;
179 }
180 const auto *e = reinterpret_cast<const esp_now_hosted_recv_evt_t *>(data);
181 if (len < sizeof(esp_now_hosted_recv_evt_t) + e->data_len) {
182 ESP_LOGW(TAG, "RECV data_len %u exceeds frame", e->data_len);
183 return;
184 }
185
186 // ESPHome dereferences info->rx_ctrl->{rssi,timestamp}; give it a real one.
187 wifi_pkt_rx_ctrl_t rx_ctrl;
188 memset(&rx_ctrl, 0, sizeof(rx_ctrl));
189 rx_ctrl.rssi = e->rssi;
190 rx_ctrl.channel = e->channel;
191 rx_ctrl.timestamp = static_cast<uint32_t>(esp_timer_get_time());
192
193 esp_now_recv_info_t info;
194 info.src_addr = const_cast<uint8_t *>(e->src_addr);
195 info.des_addr = const_cast<uint8_t *>(e->des_addr);
196 info.rx_ctrl = &rx_ctrl;
197 cb(&info, e->data, static_cast<int>(e->data_len));
198}
199
200void on_send(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) {
201 // Read the volatile pointer once (see on_recv): disable()/deinit() can null it
202 // on the main loop concurrently with this RX-thread callback.
203 const esp_now_send_cb_t cb = g_send_cb;
204 if (cb == nullptr)
205 return;
206 if (len < sizeof(esp_now_hosted_send_evt_t)) {
207 ESP_LOGW(TAG, "SEND evt too short: %u bytes", static_cast<unsigned>(len));
208 return;
209 }
210 const auto *e = reinterpret_cast<const esp_now_hosted_send_evt_t *>(data);
211#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
212 // IDF >= 5.5: esp_now_send_cb_t takes esp_now_send_info_t (== wifi_tx_info_t),
213 // whose des_addr is a POINTER (not an inline array). Point it at the event's
214 // MAC (valid for this callback) — do NOT memcpy into it (that writes NULL and
215 // faults). ESPHome reads only info->des_addr.
216 esp_now_send_info_t si;
217 memset(&si, 0, sizeof(si));
218 si.des_addr = const_cast<uint8_t *>(e->des_addr);
219 cb(&si, static_cast<esp_now_send_status_t>(e->status));
220#else
221 cb(e->des_addr, static_cast<esp_now_send_status_t>(e->status));
222#endif
223}
224
225esp_err_t ensure_setup() {
226 // Gate on g_setup_done, not on g_req_mutex: a failure part-way through (a
227 // semaphore that did not allocate, a callback that did not register) must not
228 // leave a later call thinking setup completed. Semaphore creation is guarded
229 // so a retry after a partial failure does not leak the earlier handles.
230 if (g_setup_done)
231 return ESP_OK;
232 if (g_req_mutex == nullptr)
233 g_req_mutex = xSemaphoreCreateMutex();
234 if (g_resp_sem == nullptr)
235 g_resp_sem = xSemaphoreCreateBinary();
236 if (g_req_mutex == nullptr || g_resp_sem == nullptr)
237 return ESP_ERR_NO_MEM;
238 esp_err_t err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr);
239 if (err != ESP_OK)
240 return err;
241 err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr);
242 if (err != ESP_OK)
243 return err;
244 err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr);
245 if (err != ESP_OK)
246 return err;
247 g_setup_done = true;
248 return ESP_OK;
249}
250
251// Send one request envelope. With wait=true (default) block until the matching
252// response (or timeout); with wait=false return as soon as the frame is handed
253// to the transport (fire-and-forget, used by esp_now_send).
254//
255// `tail` is an optional second chunk written straight after `payload`. Callers
256// with a fixed header plus a bulk body (esp_now_send) pass the two separately
257// so they never need a build buffer of their own: both chunks are laid into the
258// request buffer here, under g_req_mutex, which keeps concurrent callers from
259// racing and saves a full copy of the body on every transmit.
260esp_err_t request(uint8_t opcode, const void *payload, uint16_t plen, void *ret, uint16_t ret_cap, uint16_t *ret_len,
261 bool wait = true, const void *tail = nullptr, uint16_t tail_len = 0) {
262 esp_err_t err = ensure_setup();
263 if (err != ESP_OK)
264 return err;
265 if (plen > ESP_NOW_HOSTED_MAX_PAYLOAD || tail_len > ESP_NOW_HOSTED_MAX_PAYLOAD - plen)
266 return ESP_ERR_INVALID_SIZE;
267 const uint16_t total_len = static_cast<uint16_t>(plen + tail_len);
268
269 if (xSemaphoreTake(g_req_mutex, portMAX_DELAY) != pdTRUE)
270 return ESP_FAIL;
271
272 static uint8_t buf[sizeof(esp_now_hosted_req_t) + ESP_NOW_HOSTED_MAX_PAYLOAD]; // guarded by g_req_mutex
273 auto *req = reinterpret_cast<esp_now_hosted_req_t *>(buf);
274 req->opcode = opcode;
275 req->seq = ++g_seq;
276 req->payload_len = total_len;
277 if (plen != 0)
278 memcpy(req->payload, payload, plen);
279 if (tail_len != 0)
280 memcpy(req->payload + plen, tail, tail_len);
281 g_expect_seq = req->seq;
282
283 xSemaphoreTake(g_resp_sem, 0); // drain any stale signal before sending
284 err = esp_hosted_send_custom_data(ESP_NOW_HOSTED_MSG_REQ, buf, sizeof(esp_now_hosted_req_t) + total_len);
285 if (err != ESP_OK) {
286 xSemaphoreGive(g_req_mutex);
287 return err;
288 }
289 if (!wait) {
290 // Fire-and-forget (esp_now_send): the co-processor enqueues the frame and
291 // reports the real TX result later via the async SEND event, exactly like
292 // native esp_now_send. Returning here keeps the main loop off the ~100 ms+
293 // RPC round-trip. The matching RESP is ignored (seq won't match the next
294 // waited request, so on_resp drops it).
295 xSemaphoreGive(g_req_mutex);
296 return ESP_OK;
297 }
298 if (xSemaphoreTake(g_resp_sem, pdMS_TO_TICKS(ESP_NOW_HOSTED_TIMEOUT_MS)) != pdTRUE) {
299 ESP_LOGW(TAG, "opcode %u timed out", opcode);
300 xSemaphoreGive(g_req_mutex);
301 return ESP_ERR_TIMEOUT;
302 }
303
304 const int32_t status = g_resp_status;
305 if (ret != nullptr && ret_cap != 0) {
306 uint16_t n = g_resp_ret_len < ret_cap ? g_resp_ret_len : ret_cap;
307 memcpy(ret, const_cast<const uint8_t *>(g_resp_ret), n);
308 if (ret_len != nullptr)
309 *ret_len = n;
310 }
311 xSemaphoreGive(g_req_mutex);
312 return static_cast<esp_err_t>(status);
313}
314
315} // namespace
316
317// ── The <esp_now.h> surface, defined for the radio-less host ────────────────
318extern "C" {
319
320esp_err_t esp_now_init(void) { return request(ESP_NOW_HOSTED_OP_INIT, nullptr, 0, nullptr, 0, nullptr); }
321
322esp_err_t esp_now_deinit(void) {
323 g_recv_cb = nullptr;
324 g_send_cb = nullptr;
325 peer_cache_clear(); // the co-processor drops all peers on deinit
326 return request(ESP_NOW_HOSTED_OP_DEINIT, nullptr, 0, nullptr, 0, nullptr);
327}
328
329esp_err_t esp_now_get_version(uint32_t *version) {
330 uint32_t v = 0;
331 uint16_t rl = 0;
332 esp_err_t err = request(ESP_NOW_HOSTED_OP_GET_VERSION, nullptr, 0, &v, sizeof(v), &rl);
333 if (version != nullptr)
334 *version = v;
335 return err;
336}
337
338esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t cb) {
339 // Only arm the callback once the CustomRpc handlers are actually registered,
340 // so a failed setup leaves g_recv_cb null rather than falsely "registered".
341 esp_err_t err = ensure_setup();
342 if (err != ESP_OK)
343 return err;
344 g_recv_cb = cb;
345 return ESP_OK;
346}
348 g_recv_cb = nullptr;
349 return ESP_OK;
350}
351esp_err_t esp_now_register_send_cb(esp_now_send_cb_t cb) {
352 esp_err_t err = ensure_setup();
353 if (err != ESP_OK)
354 return err;
355 g_send_cb = cb;
356 return ESP_OK;
357}
359 g_send_cb = nullptr;
360 return ESP_OK;
361}
362
363static esp_err_t add_or_mod_peer(uint8_t opcode, const esp_now_peer_info_t *peer, bool wait) {
364 if (peer == nullptr)
365 return ESP_ERR_ESPNOW_ARG;
366 esp_now_hosted_peer_t p;
367 memset(&p, 0, sizeof(p));
368 memcpy(p.peer_addr, peer->peer_addr, 6);
369 memcpy(p.lmk, peer->lmk, 16);
370 p.channel = peer->channel;
371 p.ifidx = static_cast<uint8_t>(peer->ifidx);
372 p.encrypt = peer->encrypt ? 1 : 0;
373 return request(opcode, &p, sizeof(p), nullptr, 0, nullptr, wait);
374}
375esp_err_t esp_now_add_peer(const esp_now_peer_info_t *peer) {
376 // Fire-and-forget (wait=false): adding a peer is a blocking RPC round-trip,
377 // and ESPHome's espnow calls it on the main loop when a device joins the mesh
378 // — under co-processor load that stalls the UI (peer-churn stutter). Issue it
379 // without waiting and mirror it locally. Safe against a following
380 // esp_now_send to the same peer: both ride the same in-order CustomRpc
381 // channel (mutex-serialized on the host) and the co-processor processes REQs
382 // FIFO, so ADD_PEER is applied before the SEND. Trade-off: a co-processor-side
383 // failure (e.g. peer table full) is no longer reported synchronously — the
384 // same limitation as esp_now_send — but ESPHome only adds peers it validated.
385 esp_err_t err = add_or_mod_peer(ESP_NOW_HOSTED_OP_ADD_PEER, peer, /*wait=*/false);
386 if (err == ESP_OK)
387 peer_cache_add(peer->peer_addr); // keep the local mirror in sync
388 return err;
389}
390esp_err_t esp_now_mod_peer(const esp_now_peer_info_t *peer) {
391 // mod_peer changes a peer's parameters, not its existence, so the cache is
392 // unaffected. Kept synchronous — it is not on any hot path (espnow never
393 // calls it), so the extra round-trip does not matter and the status is useful.
394 return add_or_mod_peer(ESP_NOW_HOSTED_OP_MOD_PEER, peer, /*wait=*/true);
395}
396
397esp_err_t esp_now_del_peer(const uint8_t *peer_addr) {
398 if (peer_addr == nullptr)
399 return ESP_ERR_ESPNOW_ARG;
400 // Fire-and-forget for the same reason as add_peer (peer churn on the main
401 // loop). Removal is order-independent, so this is strictly safe.
402 esp_err_t err = request(ESP_NOW_HOSTED_OP_DEL_PEER, peer_addr, 6, nullptr, 0, nullptr, /*wait=*/false);
403 if (err == ESP_OK)
404 peer_cache_remove(peer_addr); // keep the local mirror in sync
405 return err;
406}
407
408bool esp_now_is_peer_exist(const uint8_t *peer_addr) {
409 if (peer_addr == nullptr)
410 return false;
411 // Answered from the local mirror — no RPC round-trip. ESPHome's espnow calls
412 // this on the main loop for every received frame and every send, so a
413 // blocking round-trip here would stall rendering under mesh traffic.
414 return peer_cache_contains(peer_addr);
415}
416
417esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len) {
418 if (len > ESP_NOW_HOSTED_MAX_FRAME)
419 return ESP_ERR_ESPNOW_ARG;
420 if (data == nullptr && len != 0) // native esp_now_send treats this as an arg error
421 return ESP_ERR_ESPNOW_ARG;
422 // Only the small fixed header is built here; the caller's frame goes over as
423 // the request tail, so request() lays both into its own buffer under
424 // g_req_mutex. esp_now_send is a public C symbol and may be called from any
425 // task, and a shared build buffer here would let two callers corrupt each
426 // other's frame. Passing the body through also drops a full-frame copy per
427 // transmit, on the path this shim exists to keep quick.
428 uint8_t hdr[sizeof(esp_now_hosted_send_req_t)] = {};
429 auto *s = reinterpret_cast<esp_now_hosted_send_req_t *>(hdr);
430 s->has_addr = peer_addr != nullptr ? 1 : 0;
431 if (peer_addr != nullptr) {
432 memcpy(s->peer_addr, peer_addr, 6);
433 } else {
434 memset(s->peer_addr, 0, 6);
435 }
436 s->data_len = static_cast<uint16_t>(len);
437 // Fire-and-forget (wait=false): native esp_now_send returns once the frame is
438 // queued, with the real TX result delivered later through the send callback.
439 // The co-processor mirrors that — it acks enqueue immediately and reports the
440 // outcome via the async SEND event (on_send -> on_send_report). Waiting for
441 // the RPC RESP here would block the main loop for the full round-trip on
442 // every transmit.
443 return request(ESP_NOW_HOSTED_OP_SEND, hdr, sizeof(hdr), nullptr, 0, nullptr, /*wait=*/false, data,
444 static_cast<uint16_t>(len));
445}
446
447esp_err_t esp_now_set_pmk(const uint8_t *pmk) {
448 if (pmk == nullptr)
449 return ESP_ERR_ESPNOW_ARG;
450 return request(ESP_NOW_HOSTED_OP_SET_PMK, pmk, 16, nullptr, 0, nullptr);
451}
452
453// Remainder of the <esp_now.h> surface. Not used by ESPHome's espnow component
454// today; provided so the whole header links and future callers get a defined
455// (if unimplemented) symbol rather than a link error. Wire them through
456// CustomRpc if a use case appears.
457esp_err_t esp_now_get_peer(const uint8_t * /*peer_addr*/, esp_now_peer_info_t * /*peer*/) {
458 return ESP_ERR_NOT_SUPPORTED;
459}
460esp_err_t esp_now_fetch_peer(bool /*from_head*/, esp_now_peer_info_t * /*peer*/) { return ESP_ERR_NOT_SUPPORTED; }
461esp_err_t esp_now_get_peer_num(esp_now_peer_num_t * /*num*/) { return ESP_ERR_NOT_SUPPORTED; }
462esp_err_t esp_now_set_wake_window(uint16_t /*window*/) {
463 return ESP_ERR_NOT_SUPPORTED; // power-save wake window is not forwarded; don't claim success
464}
465esp_err_t esp_now_set_peer_rate_config(const uint8_t * /*peer_addr*/, esp_now_rate_config_t * /*cfg*/) {
466 return ESP_ERR_NOT_SUPPORTED;
467}
468esp_err_t esp_wifi_config_espnow_rate(wifi_interface_t /*ifx*/, wifi_phy_rate_t /*rate*/) {
469 return ESP_ERR_NOT_SUPPORTED;
470}
471
472} // extern "C"
473
474#endif // CONFIG_IDF_TARGET_ESP32P4
uint8_t status
Definition bl0942.h:8
esp_err_t esp_now_del_peer(const uint8_t *peer_addr)
esp_err_t esp_now_get_version(uint32_t *version)
esp_err_t esp_now_unregister_recv_cb(void)
esp_err_t esp_wifi_config_espnow_rate(wifi_interface_t, wifi_phy_rate_t)
esp_err_t esp_now_unregister_send_cb(void)
esp_err_t esp_now_register_send_cb(esp_now_send_cb_t cb)
esp_err_t esp_now_deinit(void)
esp_err_t esp_now_get_peer_num(esp_now_peer_num_t *)
esp_err_t esp_now_add_peer(const esp_now_peer_info_t *peer)
esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len)
esp_err_t esp_now_set_pmk(const uint8_t *pmk)
esp_err_t esp_now_set_peer_rate_config(const uint8_t *, esp_now_rate_config_t *)
bool esp_now_is_peer_exist(const uint8_t *peer_addr)
esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t cb)
esp_err_t esp_now_mod_peer(const esp_now_peer_info_t *peer)
esp_err_t esp_now_get_peer(const uint8_t *, esp_now_peer_info_t *)
esp_err_t esp_now_set_wake_window(uint16_t)
esp_err_t esp_now_init(void)
esp_err_t esp_now_fetch_peer(bool, esp_now_peer_info_t *)
@ ESP_NOW_HOSTED_OP_INIT
@ ESP_NOW_HOSTED_OP_SET_PMK
@ ESP_NOW_HOSTED_OP_DEL_PEER
@ ESP_NOW_HOSTED_OP_GET_VERSION
@ ESP_NOW_HOSTED_OP_DEINIT
@ ESP_NOW_HOSTED_OP_ADD_PEER
@ ESP_NOW_HOSTED_OP_SEND
@ ESP_NOW_HOSTED_OP_MOD_PEER
ESPHOME_LOG_TAG(TAG, "hw_timer_esp_idf")
int ret
std::span< const uint8_t > data
num_t cb(num_t x)
Definition sun.cpp:29
int64_t esp_timer_get_time(void)
static void uint32_t
uint32_t len