ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
espnow_component.cpp
Go to the documentation of this file.
1#include "espnow_component.h"
2
3#if defined(USE_ESP32) || defined(USE_ESP8266)
4
5#include "espnow_err.h"
6#include "espnow_platform.h"
7
8#include <algorithm>
9#include <cinttypes>
10#include <cstring>
11#include <utility>
12
15#include "esphome/core/log.h"
16
17#ifdef USE_WIFI
19#endif
20
21namespace esphome::espnow {
22
23ESPHOME_LOG_TAG(TAG, "espnow");
24
25ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
26
27static const LogString *espnow_error_to_str(esp_err_t error) {
28 switch (error) {
29 case ESP_ERR_ESPNOW_FAILED:
30 return LOG_STR("ESPNow is in fail mode");
31 case ESP_ERR_ESPNOW_OWN_ADDRESS:
32 return LOG_STR("Message to your self");
33 case ESP_ERR_ESPNOW_DATA_SIZE:
34 return LOG_STR("Data size to large");
35 case ESP_ERR_ESPNOW_PEER_NOT_SET:
36 return LOG_STR("Peer address not set");
37 case ESP_ERR_ESPNOW_PEER_NOT_PAIRED:
38 return LOG_STR("Peer address not paired");
39 case ESP_ERR_ESPNOW_NOT_INIT:
40 return LOG_STR("Not init");
41 case ESP_ERR_ESPNOW_ARG:
42 return LOG_STR("Invalid argument");
43 case ESP_ERR_ESPNOW_INTERNAL:
44 return LOG_STR("Internal Error");
45 case ESP_ERR_ESPNOW_NO_MEM:
46 return LOG_STR("Out of memory");
47 case ESP_ERR_ESPNOW_NOT_FOUND:
48 return LOG_STR("Peer not found");
49 case ESP_ERR_ESPNOW_IF:
50 return LOG_STR("Interface does not match");
51 case ESP_OK:
52 return LOG_STR("OK");
54 case ESP_FAIL:
55 return LOG_STR("Failed");
56 default:
57 return LOG_STR("Unknown Error");
58 }
59}
60
61// The analyzer traces a leak on the failed-push path, which cannot happen: the
62// pools are sized to the queue capacity (SIZE-1), so allocate() returns nullptr
63// before push() can find the ring full.
64// NOLINTBEGIN(clang-analyzer-unix.Malloc)
65void ESPNowComponent::send_reported(const uint8_t *mac_addr, esp_now_send_status_t status) {
66 // Allocate an event from the pool
67 ESPNowPacket *packet = this->receive_packet_pool_.allocate();
68 if (packet == nullptr) {
69 // No events available - queue is full or we're out of memory
70 this->receive_packet_queue_.increment_dropped_count();
71 this->enable_loop_soon_any_context();
72 return;
73 }
74
75 packet->load_sent_data(mac_addr, status);
76
77 // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
78 // allocate() returned non-null, the queue cannot be full.
79 this->receive_packet_queue_.push(packet);
80
81 // Re-enable and wake the main loop to process the ESP-NOW send event
82 this->enable_loop_soon_any_context();
83}
84
85void ESPNowComponent::packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size,
86 int8_t rssi, uint32_t timestamp) {
87 // Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
88 // v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
89 // but the receive buffer only fits frames up to ``max_payload_size``; copying a
90 // larger frame would overflow packet_.receive.data.
91 if (size < 0 || static_cast<size_t>(size) > ESPNOW_MAX_DATA_LEN) {
92 this->receive_packet_queue_.increment_dropped_count();
93 this->enable_loop_soon_any_context();
94 return;
95 }
96
97 // Allocate an event from the pool
98 ESPNowPacket *packet = this->receive_packet_pool_.allocate();
99 if (packet == nullptr) {
100 // No events available - queue is full or we're out of memory
101 this->receive_packet_queue_.increment_dropped_count();
102 this->enable_loop_soon_any_context();
103 return;
104 }
105
106 packet->load_received_data(src_addr, des_addr, data, size, rssi, timestamp);
107
108 // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
109 // allocate() returned non-null, the queue cannot be full.
110 this->receive_packet_queue_.push(packet);
111
112 // Re-enable and wake the main loop to process the ESP-NOW receive event
113 this->enable_loop_soon_any_context();
114}
115// NOLINTEND(clang-analyzer-unix.Malloc)
116
117ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
118
119void ESPNowComponent::dump_config() {
120 ESP_LOGCONFIG(TAG, "espnow:");
121 // Only report driver details once enabled; with enable_on_boot: false the
122 // Wi-Fi driver is not initialized yet and the version call would crash,
123 // and after a failed enable_() the values would be meaningless.
124 if (this->state_ != ESPNOW_STATE_ENABLED) {
125 // OFF here means enable_() failed; the core logs the FAILED marker separately
126 ESP_LOGCONFIG(TAG, " %s", this->is_disabled() ? LOG_STR_LITERAL("Disabled") : LOG_STR_LITERAL("Not enabled"));
127 return;
128 }
129 char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
130 format_mac_addr_upper(this->own_address_, own_addr_buf);
131 ESP_LOGCONFIG(TAG,
132 " Own address: %s\n"
133 " Wi-Fi channel: %d",
134 own_addr_buf, this->wifi_channel_);
136 if (version != 0) {
137 ESP_LOGCONFIG(TAG, " Version: v%" PRIu32, version);
138 }
139#ifdef USE_WIFI
140 ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled()));
141#endif
142}
143
144bool ESPNowComponent::is_wifi_enabled() {
145#ifdef USE_WIFI
147#else
148 return false;
149#endif
150}
151
152void ESPNowComponent::setup() {
153#if defined(USE_WIFI) && defined(USE_WIFI_CONNECT_STATE_LISTENERS)
154 if (wifi::global_wifi_component != nullptr) {
156 }
157#endif
158 if (this->enable_on_boot_) {
159 this->enable_();
160 } else {
161 this->state_ = ESPNOW_STATE_DISABLED;
162 }
163}
164
165#if defined(USE_WIFI) && defined(USE_WIFI_CONNECT_STATE_LISTENERS)
166void ESPNowComponent::on_wifi_connect_state(StringRef ssid, std::span<const uint8_t, 6> bssid) {
167 if (ssid.empty()) {
168 return; // Disconnected; the channel is only meaningful while associated
169 }
170 uint8_t old_channel = this->wifi_channel_;
171 this->get_wifi_channel();
172 if (this->wifi_channel_ != old_channel) {
173 ESP_LOGI(TAG, "WiFi channel changed from %d to %d", old_channel, this->wifi_channel_);
174 }
175 // Peers added while the station was scanning were bound to the channel of that moment, so rebind on every connect
176 if (this->state_ == ESPNOW_STATE_ENABLED) {
178 }
179}
180#endif
181
182void ESPNowComponent::enable() {
183 if (this->state_ == ESPNOW_STATE_ENABLED)
184 return;
185
186 ESP_LOGD(TAG, "Enabling");
187 this->state_ = ESPNOW_STATE_OFF;
188
189 this->enable_();
190}
191
192void ESPNowComponent::enable_() {
193 if (!this->is_wifi_enabled()) {
195 this->apply_wifi_channel();
196 }
197 this->get_wifi_channel();
198
200 if (err != ESP_OK) {
201 ESP_LOGE(TAG, "Init failed: %s", esp_err_to_name(err));
202 this->mark_failed();
203 return;
204 }
205
206 platform::read_mac(this->own_address_);
207
208 this->state_ = ESPNOW_STATE_ENABLED;
209
210 for (auto peer : this->peers_) {
211 this->add_peer(peer.address);
212 }
213}
214
215void ESPNowComponent::disable() {
216 if (this->state_ == ESPNOW_STATE_DISABLED)
217 return;
218
219 ESP_LOGD(TAG, "Disabling");
220 this->state_ = ESPNOW_STATE_DISABLED;
221
223 if (err != ESP_OK) {
224 ESP_LOGE(TAG, "Deinit failed: %s", esp_err_to_name(err));
225 }
226}
227
228void ESPNowComponent::apply_wifi_channel() {
229 if (this->state_ == ESPNOW_STATE_DISABLED) {
230 ESP_LOGE(TAG, "Cannot set channel when ESPNOW disabled");
231 this->mark_failed();
232 return;
233 }
234
235 if (this->is_wifi_enabled()) {
236 ESP_LOGE(TAG, "Cannot set channel when Wi-Fi enabled");
237 this->mark_failed();
238 return;
239 }
240
241 ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_);
242 platform::set_channel(this->wifi_channel_);
243 if (this->state_ == ESPNOW_STATE_ENABLED) {
245 }
246}
247
248void ESPNowComponent::loop() {
249 // Process received packets
250 ESPNowPacket *packet = this->receive_packet_queue_.pop();
251 while (packet != nullptr) {
252 switch (packet->type_) {
254 const ESPNowRecvInfo info = packet->get_receive_info();
255 if (!platform::peer_exists(info.src_addr)) {
256 bool handled = false;
257 for (auto *handler : this->unknown_peer_handlers_) {
258 if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) {
259 handled = true;
260 break; // If a handler returns true, stop processing further handlers
261 }
262 }
263 if (!handled && this->auto_add_peer_) {
264 this->add_peer(info.src_addr);
265 }
266 }
267 // Intentionally left as if instead of else in case the peer is added above
268 if (platform::peer_exists(info.src_addr)) {
269#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
270 char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
271 char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
272 // Cap the hex dump at a v1 frame: a full v2 frame would need a
273 // ~4.4 KB stack buffer.
274 char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)];
275 format_mac_addr_upper(info.src_addr, src_buf);
276 format_mac_addr_upper(info.des_addr, dst_buf);
277 ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf,
278 format_hex_pretty_to(hex_buf, packet->packet_.receive.data,
279 std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
280#endif
281 // Without a destination address every packet goes to the receive handlers
282 if (ESPNOW_REPORTS_DESTINATION && memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
283 for (auto *handler : this->broadcast_handlers_) {
284 if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
285 break; // If a handler returns true, stop processing further handlers
286 }
287 } else {
288 for (auto *handler : this->receive_handlers_) {
289 if (handler->on_receive(info, packet->packet_.receive.data, packet->packet_.receive.size))
290 break; // If a handler returns true, stop processing further handlers
291 }
292 }
293 }
294 break;
295 }
296 case ESPNowPacket::SENT: {
297#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
298 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
299 format_mac_addr_upper(packet->packet_.sent.address, addr_buf);
300 ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
301#endif
302 if (this->current_send_packet_ != nullptr) {
303 if (this->current_send_packet_->callback_ != nullptr) {
304 this->current_send_packet_->callback_(packet->packet_.sent.status);
305 }
306 this->send_packet_pool_.release(this->current_send_packet_);
307 this->current_send_packet_ = nullptr; // Reset current packet after sending
308 }
309 break;
310 }
311 default:
312 break;
313 }
314 // Return the packet to the pool
315 this->receive_packet_pool_.release(packet);
316 packet = this->receive_packet_queue_.pop();
317 }
318
319 // Process sending packet queue
320 if (this->current_send_packet_ == nullptr) {
321 this->send_();
322 }
323
324 // Log dropped received packets periodically
325 uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count();
326 if (received_dropped > 0) {
327 ESP_LOGW(TAG, "Dropped %u received packets (queue full or oversized frame)", received_dropped);
328 }
329
330 // Log dropped send packets periodically
331 uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count();
332 if (send_dropped > 0) {
333 ESP_LOGW(TAG, "Dropped %u send packets (queue full)", send_dropped);
334 }
335
336 // Nothing left to do; sleep until a callback or send() re-enables the loop.
337 // A packet in flight (current_send_packet_) needs no loop time even when more
338 // packets are queued behind it: the send callback re-enables the loop when
339 // the result arrives, and the SENT event handler above starts the next send.
340 if (this->receive_packet_queue_.empty() &&
341 (this->current_send_packet_ != nullptr || this->send_packet_queue_.empty())) {
342 this->disable_loop();
343 }
344}
345
346uint8_t ESPNowComponent::get_wifi_channel() {
347 this->wifi_channel_ = platform::get_channel();
348 return this->wifi_channel_;
349}
350
351// Same analyzer false positive as above: the send pool is sized to the send queue's capacity
352// NOLINTBEGIN(clang-analyzer-unix.Malloc)
353esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
354 send_callback_t callback) {
355 if (this->state_ != ESPNOW_STATE_ENABLED) {
356 return ESP_ERR_ESPNOW_NOT_INIT;
357 } else if (this->is_failed()) {
358 return ESP_ERR_ESPNOW_FAILED;
359 } else if (peer_address == nullptr) {
360 return ESP_ERR_ESPNOW_PEER_NOT_SET;
361 } else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
362 return ESP_ERR_ESPNOW_OWN_ADDRESS;
363 } else if (size > ESPNOW_MAX_DATA_LEN) {
364 return ESP_ERR_ESPNOW_DATA_SIZE;
365 } else if (!platform::peer_exists(peer_address)) {
366 if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
367 esp_err_t err = this->add_peer(peer_address);
368 if (err != ESP_OK) {
369 return err;
370 }
371 } else {
372 return ESP_ERR_ESPNOW_PEER_NOT_PAIRED;
373 }
374 }
375 // Allocate a packet from the pool
376 ESPNowSendPacket *packet = this->send_packet_pool_.allocate();
377 if (packet == nullptr) {
378 this->send_packet_queue_.increment_dropped_count();
379 ESP_LOGE(TAG, "Failed to allocate send packet from pool");
380 this->status_momentary_warning();
381 return ESP_ERR_ESPNOW_NO_MEM;
382 }
383 // Load the packet data
384 packet->load_data(peer_address, payload, size, std::move(callback));
385 // Push the packet to the send queue
386 this->send_packet_queue_.push(packet);
387 // Loop may be disabled while idle; re-enable it to send the packet
388 // (any-context variant so callers off the main loop are safe too)
389 this->enable_loop_soon_any_context();
390 return ESP_OK;
391}
392// NOLINTEND(clang-analyzer-unix.Malloc)
393
394void ESPNowComponent::send_() {
395 ESPNowSendPacket *packet = this->send_packet_queue_.pop();
396 if (packet == nullptr) {
397 return; // No packets to send
398 }
399
400 this->current_send_packet_ = packet;
401 esp_err_t err = platform::send(packet->address_, packet->data_, packet->size_);
402 if (err != ESP_OK) {
403 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
404 format_mac_addr_upper(packet->address_, addr_buf);
405 ESP_LOGE(TAG, "Failed to send packet to %s - %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(err)));
406 if (packet->callback_ != nullptr) {
407 packet->callback_(err);
408 }
409 this->status_momentary_warning();
410 this->send_packet_pool_.release(packet);
411 this->current_send_packet_ = nullptr; // Reset current packet
412 return;
413 }
414}
415
416esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
417 if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
418 return ESP_ERR_ESPNOW_NOT_INIT;
419 }
420
421 if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
422 this->status_momentary_warning();
423 return ESP_ERR_INVALID_MAC;
424 }
425
426 if (!platform::peer_exists(peer)) {
427 esp_err_t err = platform::add_peer(peer);
428 if (err != ESP_OK) {
429 char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
430 format_mac_addr_upper(peer, peer_buf);
431 ESP_LOGE(TAG, "Failed to add peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err)));
432 this->status_momentary_warning();
433 return err;
434 }
435 }
436 bool found = false;
437 for (auto &it : this->peers_) {
438 if (it == peer) {
439 found = true;
440 break;
441 }
442 }
443 if (!found) {
444 ESPNowPeer new_peer;
445 memcpy(new_peer.address, peer, ESP_NOW_ETH_ALEN);
446 this->peers_.push_back(new_peer);
447 }
448
449 return ESP_OK;
450}
451
452esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
453 if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
454 return ESP_ERR_ESPNOW_NOT_INIT;
455 }
456 if (platform::peer_exists(peer)) {
457 esp_err_t err = platform::del_peer(peer);
458 if (err != ESP_OK) {
459 char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
460 format_mac_addr_upper(peer, peer_buf);
461 ESP_LOGE(TAG, "Failed to delete peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err)));
462 this->status_momentary_warning();
463 return err;
464 }
465 }
466 for (auto it = this->peers_.begin(); it != this->peers_.end(); ++it) {
467 if (*it == peer) {
468 this->peers_.erase(it);
469 break;
470 }
471 }
472 return ESP_OK;
473}
474
475} // namespace esphome::espnow
476
477#endif // USE_ESP32 || USE_ESP8266
void add_connect_state_listener(WiFiConnectStateListener *listener)
Add a listener for WiFi connection state changes.
esp_err_t del_peer(const uint8_t *mac)
esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size)
void set_channel(uint8_t channel)
bool peer_exists(const uint8_t *mac)
esp_err_t add_peer(const uint8_t *mac)
ESPNowComponent * global_esp_now
const char * esp_err_to_name(esp_err_t err)
ESPHOME_LOG_TAG(TAG, "espnow")
std::function< void(esp_err_t)> send_callback_t
bool is_disabled()
Return whether the network is disabled: every configured interface with a disable() lifecycle (modem,...
Definition util.cpp:42
WiFiComponent * global_wifi_component
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:498
uint16_t size
Definition helpers.cpp:34
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1609
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1676
static void uint32_t
uint16_t timestamp
Definition tt21100.cpp:2