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