From 6bc19d2a1aafc38f0650d70d4ab0b22dc4b3aac9 Mon Sep 17 00:00:00 2001 From: Mathew Winstone Date: Sat, 10 Oct 2026 23:45:03 -0400 Subject: [PATCH] [espnow] Add support for ESPNow with ESP8266 (#14407) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- esphome/components/espnow/__init__.py | 31 +++- esphome/components/espnow/automation.h | 4 +- .../components/espnow/espnow_component.cpp | 167 +++++++----------- esphome/components/espnow/espnow_component.h | 35 ++-- esphome/components/espnow/espnow_err.h | 19 +- esphome/components/espnow/espnow_packet.h | 70 ++------ esphome/components/espnow/espnow_platform.h | 34 ++++ .../espnow/espnow_platform_esp32.cpp | 96 ++++++++++ .../espnow/espnow_platform_esp8266.cpp | 105 +++++++++++ esphome/components/espnow/espnow_types.h | 77 ++++++++ .../packet_transport/espnow_transport.cpp | 14 +- .../packet_transport/espnow_transport.h | 5 +- esphome/core/event_pool.h | 3 +- tests/components/espnow/common-esp32.yaml | 9 + tests/components/espnow/common.yaml | 11 +- .../espnow/test-wifi.esp8266-ard.yaml | 2 + tests/components/espnow/test.esp32-idf.yaml | 4 +- tests/components/espnow/test.esp8266-ard.yaml | 2 + tests/unit_tests/components/test_espnow.py | 53 +++++- 19 files changed, 523 insertions(+), 218 deletions(-) create mode 100644 esphome/components/espnow/espnow_platform.h create mode 100644 esphome/components/espnow/espnow_platform_esp32.cpp create mode 100644 esphome/components/espnow/espnow_platform_esp8266.cpp create mode 100644 esphome/components/espnow/espnow_types.h create mode 100644 tests/components/espnow/common-esp32.yaml create mode 100644 tests/components/espnow/test-wifi.esp8266-ard.yaml create mode 100644 tests/components/espnow/test.esp8266-ard.yaml diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index aef8fd17b56..e5abb16b1c3 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -17,6 +17,8 @@ from esphome.const import ( CONF_ON_ERROR, CONF_TRIGGER_ID, CONF_WIFI, + PLATFORM_ESP32, + PLATFORM_ESP8266, ) from esphome.core import CORE, HexInt from esphome.cpp_generator import MockObj, TemplateArgsType @@ -24,7 +26,14 @@ import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] -AUTO_LOAD = ["network"] + + +def AUTO_LOAD() -> list[str]: + # network initialises esp_netif and the default event loop on the ESP32. The ESP8266 needs neither, and + # loading it there would pull in mdns, which requires wifi + return ["network"] if CORE.is_esp32 else [] + + DOMAIN = "espnow" peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -121,15 +130,21 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ADDRESS): cv.mac_address, } ), - cv.Optional(CONF_ON_BROADCAST): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger), - cv.Optional(CONF_ADDRESS): cv.mac_address, - } + # The ESP8266 SDK does not report the destination address, so broadcasts are indistinguishable there + cv.Optional(CONF_ON_BROADCAST): cv.All( + cv.only_on_esp32, + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + OnBroadcastTrigger + ), + cv.Optional(CONF_ADDRESS): cv.mac_address, + } + ), ), }, ).extend(cv.COMPONENT_SCHEMA), - cv.only_on_esp32, + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), ) @@ -138,6 +153,8 @@ def _validate_variant(config: ConfigType) -> ConfigType: # ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that # supplies the esp_now_* symbols. Fail here with a clear message instead of # letting the build reach an "undefined reference to esp_now_*" link error. + if not CORE.is_esp32: + return config variant = get_esp32_variant() if wifi.variant_has_wifi(variant): return config diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 851b8345c94..88e240a646a 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP8266) #include "espnow_component.h" @@ -142,4 +142,4 @@ class OnBroadcastTrigger final : public Trigger #include +#include +#include #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include -#include -#include -#include -#include -#include -#include - #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -49,7 +43,7 @@ static const LogString *espnow_error_to_str(esp_err_t error) { case ESP_ERR_ESPNOW_INTERNAL: return LOG_STR("Internal Error"); case ESP_ERR_ESPNOW_NO_MEM: - return LOG_STR("Our of memory"); + return LOG_STR("Out of memory"); case ESP_ERR_ESPNOW_NOT_FOUND: return LOG_STR("Peer not found"); case ESP_ERR_ESPNOW_IF: @@ -57,96 +51,91 @@ static const LogString *espnow_error_to_str(esp_err_t error) { case ESP_OK: return LOG_STR("OK"); case ESP_NOW_SEND_FAIL: + case ESP_FAIL: return LOG_STR("Failed"); default: return LOG_STR("Unknown Error"); } } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) -void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) -#else -void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) -#endif -{ +// The analyzer traces a leak on the failed-push path, which cannot happen: the +// pools are sized to the queue capacity (SIZE-1), so allocate() returns nullptr +// before push() can find the ring full. +// NOLINTBEGIN(clang-analyzer-unix.Malloc) +void ESPNowComponent::send_reported(const uint8_t *mac_addr, esp_now_send_status_t status) { // Allocate an event from the pool - ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate(); + ESPNowPacket *packet = this->receive_packet_pool_.allocate(); if (packet == nullptr) { // No events available - queue is full or we're out of memory - global_esp_now->receive_packet_queue_.increment_dropped_count(); - global_esp_now->enable_loop_soon_any_context(); + this->receive_packet_queue_.increment_dropped_count(); + this->enable_loop_soon_any_context(); return; } -// Load new packet data (replaces previous packet) -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) - packet->load_sent_data(info->des_addr, status); -#else packet->load_sent_data(mac_addr, status); -#endif - // Push the packet to the queue - global_esp_now->receive_packet_queue_.push(packet); // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. + this->receive_packet_queue_.push(packet); // Re-enable and wake the main loop to process the ESP-NOW send event - global_esp_now->enable_loop_soon_any_context(); + this->enable_loop_soon_any_context(); } -void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { +void ESPNowComponent::packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size, + int8_t rssi, uint32_t timestamp) { // Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a // v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B), - // but the receive buffer only fits v2 frames with ``max_payload_size``; copying a + // but the receive buffer only fits frames up to ``max_payload_size``; copying a // larger frame would overflow packet_.receive.data. - if (size < 0 || size > ESPNOW_MAX_DATA_LEN) { - global_esp_now->receive_packet_queue_.increment_dropped_count(); - global_esp_now->enable_loop_soon_any_context(); + if (size < 0 || static_cast(size) > ESPNOW_MAX_DATA_LEN) { + this->receive_packet_queue_.increment_dropped_count(); + this->enable_loop_soon_any_context(); return; } // Allocate an event from the pool - ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate(); + ESPNowPacket *packet = this->receive_packet_pool_.allocate(); if (packet == nullptr) { // No events available - queue is full or we're out of memory - global_esp_now->receive_packet_queue_.increment_dropped_count(); - global_esp_now->enable_loop_soon_any_context(); + this->receive_packet_queue_.increment_dropped_count(); + this->enable_loop_soon_any_context(); return; } - // Load new packet data (replaces previous packet) - packet->load_received_data(info, data, size); + packet->load_received_data(src_addr, des_addr, data, size, rssi, timestamp); - // Push the packet to the queue - global_esp_now->receive_packet_queue_.push(packet); // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. + this->receive_packet_queue_.push(packet); // Re-enable and wake the main loop to process the ESP-NOW receive event - global_esp_now->enable_loop_soon_any_context(); + this->enable_loop_soon_any_context(); } +// NOLINTEND(clang-analyzer-unix.Malloc) ESPNowComponent::ESPNowComponent() { global_esp_now = this; } void ESPNowComponent::dump_config() { ESP_LOGCONFIG(TAG, "espnow:"); // Only report driver details once enabled; with enable_on_boot: false the - // Wi-Fi driver is not initialized yet and esp_now_get_version() would crash, + // Wi-Fi driver is not initialized yet and the version call would crash, // and after a failed enable_() the values would be meaningless. if (this->state_ != ESPNOW_STATE_ENABLED) { // OFF here means enable_() failed; the core logs the FAILED marker separately ESP_LOGCONFIG(TAG, " %s", this->is_disabled() ? LOG_STR_LITERAL("Disabled") : LOG_STR_LITERAL("Not enabled")); return; } - uint32_t version = 0; - esp_now_get_version(&version); char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(this->own_address_, own_addr_buf); ESP_LOGCONFIG(TAG, " Own address: %s\n" - " Version: v%" PRIu32 "\n" " Wi-Fi channel: %d", - own_addr_buf, version, this->wifi_channel_); + own_addr_buf, this->wifi_channel_); + uint32_t version = platform::get_version(); + if (version != 0) { + ESP_LOGCONFIG(TAG, " Version: v%" PRIu32, version); + } #ifdef USE_WIFI ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled())); #endif @@ -183,6 +172,10 @@ void ESPNowComponent::on_wifi_connect_state(StringRef ssid, std::spanwifi_channel_ != old_channel) { ESP_LOGI(TAG, "WiFi channel changed from %d to %d", old_channel, this->wifi_channel_); } + // Peers added while the station was scanning were bound to the channel of that moment, so rebind on every connect + if (this->state_ == ESPNOW_STATE_ENABLED) { + platform::rebind_peers(); + } } #endif @@ -198,41 +191,19 @@ void ESPNowComponent::enable() { void ESPNowComponent::enable_() { if (!this->is_wifi_enabled()) { - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); - ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); - ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); - ESP_ERROR_CHECK(esp_wifi_start()); - ESP_ERROR_CHECK(esp_wifi_disconnect()); - + platform::init_radio(); this->apply_wifi_channel(); } this->get_wifi_channel(); - esp_err_t err = esp_now_init(); + esp_err_t err = platform::init(); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Init failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - err = esp_now_register_recv_cb(on_data_received); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - err = esp_now_register_send_cb(on_send_report); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - esp_wifi_get_mac(WIFI_IF_STA, this->own_address_); + platform::read_mac(this->own_address_); this->state_ = ESPNOW_STATE_ENABLED; @@ -248,12 +219,9 @@ void ESPNowComponent::disable() { ESP_LOGD(TAG, "Disabling"); this->state_ = ESPNOW_STATE_DISABLED; - esp_now_unregister_recv_cb(); - esp_now_unregister_send_cb(); - - esp_err_t err = esp_now_deinit(); + esp_err_t err = platform::deinit(); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err); + ESP_LOGE(TAG, "Deinit failed: %s", esp_err_to_name(err)); } } @@ -271,9 +239,10 @@ void ESPNowComponent::apply_wifi_channel() { } ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_); - esp_wifi_set_promiscuous(true); - esp_wifi_set_channel(this->wifi_channel_, WIFI_SECOND_CHAN_NONE); - esp_wifi_set_promiscuous(false); + platform::set_channel(this->wifi_channel_); + if (this->state_ == ESPNOW_STATE_ENABLED) { + platform::rebind_peers(); + } } void ESPNowComponent::loop() { @@ -283,7 +252,7 @@ void ESPNowComponent::loop() { switch (packet->type_) { case ESPNowPacket::RECEIVED: { const ESPNowRecvInfo info = packet->get_receive_info(); - if (!esp_now_is_peer_exist(info.src_addr)) { + if (!platform::peer_exists(info.src_addr)) { bool handled = false; for (auto *handler : this->unknown_peer_handlers_) { if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) { @@ -296,7 +265,7 @@ void ESPNowComponent::loop() { } } // Intentionally left as if instead of else in case the peer is added above - if (esp_now_is_peer_exist(info.src_addr)) { + if (platform::peer_exists(info.src_addr)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; @@ -309,7 +278,8 @@ void ESPNowComponent::loop() { format_hex_pretty_to(hex_buf, packet->packet_.receive.data, std::min(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN))); #endif - if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { + // Without a destination address every packet goes to the receive handlers + if (ESPNOW_REPORTS_DESTINATION && memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { for (auto *handler : this->broadcast_handlers_) { if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size)) break; // If a handler returns true, stop processing further handlers @@ -374,24 +344,25 @@ void ESPNowComponent::loop() { } uint8_t ESPNowComponent::get_wifi_channel() { - wifi_second_chan_t dummy; - esp_wifi_get_channel(&this->wifi_channel_, &dummy); + this->wifi_channel_ = platform::get_channel(); return this->wifi_channel_; } +// Same analyzer false positive as above: the send pool is sized to the send queue's capacity +// NOLINTBEGIN(clang-analyzer-unix.Malloc) esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size, - const send_callback_t &callback) { + send_callback_t callback) { if (this->state_ != ESPNOW_STATE_ENABLED) { return ESP_ERR_ESPNOW_NOT_INIT; } else if (this->is_failed()) { return ESP_ERR_ESPNOW_FAILED; - } else if (peer_address == 0ULL) { + } else if (peer_address == nullptr) { return ESP_ERR_ESPNOW_PEER_NOT_SET; } else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { return ESP_ERR_ESPNOW_OWN_ADDRESS; } else if (size > ESPNOW_MAX_DATA_LEN) { return ESP_ERR_ESPNOW_DATA_SIZE; - } else if (!esp_now_is_peer_exist(peer_address)) { + } else if (!platform::peer_exists(peer_address)) { if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) { esp_err_t err = this->add_peer(peer_address); if (err != ESP_OK) { @@ -410,7 +381,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl return ESP_ERR_ESPNOW_NO_MEM; } // Load the packet data - packet->load_data(peer_address, payload, size, callback); + packet->load_data(peer_address, payload, size, std::move(callback)); // Push the packet to the send queue this->send_packet_queue_.push(packet); // Loop may be disabled while idle; re-enable it to send the packet @@ -418,6 +389,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl this->enable_loop_soon_any_context(); return ESP_OK; } +// NOLINTEND(clang-analyzer-unix.Malloc) void ESPNowComponent::send_() { ESPNowSendPacket *packet = this->send_packet_queue_.pop(); @@ -426,7 +398,7 @@ void ESPNowComponent::send_() { } this->current_send_packet_ = packet; - esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_); + esp_err_t err = platform::send(packet->address_, packet->data_, packet->size_); if (err != ESP_OK) { char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(packet->address_, addr_buf); @@ -451,13 +423,8 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { return ESP_ERR_INVALID_MAC; } - if (!esp_now_is_peer_exist(peer)) { - esp_now_peer_info_t peer_info = {}; - memset(&peer_info, 0, sizeof(esp_now_peer_info_t)); - peer_info.ifidx = WIFI_IF_STA; - memcpy(peer_info.peer_addr, peer, ESP_NOW_ETH_ALEN); - esp_err_t err = esp_now_add_peer(&peer_info); - + if (!platform::peer_exists(peer)) { + esp_err_t err = platform::add_peer(peer); if (err != ESP_OK) { char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(peer, peer_buf); @@ -486,8 +453,8 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) { if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) { return ESP_ERR_ESPNOW_NOT_INIT; } - if (esp_now_is_peer_exist(peer)) { - esp_err_t err = esp_now_del_peer(peer); + if (platform::peer_exists(peer)) { + esp_err_t err = platform::del_peer(peer); if (err != ESP_OK) { char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(peer, peer_buf); @@ -507,4 +474,4 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) { } // namespace esphome::espnow -#endif // USE_ESP32 +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index 375bd4faefe..b912233eed0 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -4,21 +4,17 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP8266) #include "esphome/core/event_pool.h" #include "esphome/core/lock_free_queue.h" #include "espnow_packet.h" +#include "espnow_types.h" #if defined(USE_WIFI) && defined(USE_WIFI_CONNECT_STATE_LISTENERS) #include "esphome/components/wifi/wifi_component.h" #endif -#include - -#include -#include - #include #include #include @@ -27,10 +23,6 @@ namespace esphome::espnow { -// Maximum size of the ESPNow event queue - must be power of 2 for lock-free queue -static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16; -static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16; - using peer_address_t = std::array; enum class ESPNowTriggers : uint8_t { @@ -152,12 +144,10 @@ class ESPNowComponent final : public Component { /// @param payload Data payload to send /// @param callback Callback to call when the send operation is complete /// @return ESP_OK on success, or an error code on failure - esp_err_t send(const uint8_t *peer_address, const std::vector &payload, - const send_callback_t &callback = nullptr) { - return this->send(peer_address, payload.data(), payload.size(), callback); + esp_err_t send(const uint8_t *peer_address, const std::vector &payload, send_callback_t callback = nullptr) { + return this->send(peer_address, payload.data(), payload.size(), std::move(callback)); } - esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size, - const send_callback_t &callback = nullptr); + esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size, send_callback_t callback = nullptr); void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); } void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) { @@ -165,14 +155,13 @@ class ESPNowComponent final : public Component { } void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); } - protected: - friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) - friend void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status); -#else - friend void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status); -#endif + // Entry points for the radio driver callbacks in espnow_platform_*.cpp; they run outside the main loop and + // only queue the event + void packet_received(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, int size, int8_t rssi, + uint32_t timestamp); + void send_reported(const uint8_t *mac_addr, esp_now_send_status_t status); + protected: void enable_(); void send_(); @@ -205,4 +194,4 @@ extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-c } // namespace esphome::espnow -#endif // USE_ESP32 +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/components/espnow/espnow_err.h b/esphome/components/espnow/espnow_err.h index ceda1b7683e..8db38b800ef 100644 --- a/esphome/components/espnow/espnow_err.h +++ b/esphome/components/espnow/espnow_err.h @@ -1,19 +1,18 @@ #pragma once -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP8266) -#include -#include +#include "espnow_types.h" namespace esphome::espnow { -static const esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20); -static const esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1); -static const esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2); -static const esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3); -static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4); -static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5); +static constexpr esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20); +static constexpr esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1); +static constexpr esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2); +static constexpr esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3); +static constexpr esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4); +static constexpr esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5); } // namespace esphome::espnow -#endif // USE_ESP32 +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/components/espnow/espnow_packet.h b/esphome/components/espnow/espnow_packet.h index fb125864fb6..d9f4a73f931 100644 --- a/esphome/components/espnow/espnow_packet.h +++ b/esphome/components/espnow/espnow_packet.h @@ -1,19 +1,17 @@ #pragma once -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP8266) #include "espnow_err.h" +#include "espnow_types.h" #include #include #include #include +#include #include -#include -#include -#include - namespace esphome::espnow { static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; @@ -36,16 +34,10 @@ static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN, "espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)"); #endif -struct WifiPacketRxControl { - int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm - uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or - // light sleep is not enabled -}; - struct ESPNowRecvInfo { uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */ uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */ - wifi_pkt_rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */ + rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */ }; using send_callback_t = std::function; @@ -58,34 +50,28 @@ class ESPNowPacket { SENT, }; - // Constructor for received data - ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) { - this->init_received_data_(info, data, size); - }; - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) - // Constructor for sent data - ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) { - this->init_sent_data_(info->src_addr, status); - } -#else - // Constructor for sent data - ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); } -#endif - // Default constructor for pre-allocation in pool ESPNowPacket() {} void release() {} - void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) { + void load_received_data(const uint8_t *src_addr, const uint8_t *des_addr, const uint8_t *data, uint16_t size, + int8_t rssi, uint32_t timestamp) { this->type_ = RECEIVED; - this->init_received_data_(info, data, size); + memcpy(this->packet_.receive.info.src_addr, src_addr, ESP_NOW_ETH_ALEN); + memcpy(this->packet_.receive.info.des_addr, des_addr, ESP_NOW_ETH_ALEN); + memcpy(this->packet_.receive.data, data, size); + this->packet_.receive.size = size; + + this->packet_.receive.rx_ctrl.rssi = rssi; + this->packet_.receive.rx_ctrl.timestamp = timestamp; + this->packet_.receive.info.rx_ctrl = reinterpret_cast(&this->packet_.receive.rx_ctrl); } void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) { this->type_ = SENT; - this->init_sent_data_(mac_addr, status); + memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN); + this->packet_.sent.status = status; } // Disable copy to prevent double-delete @@ -112,24 +98,6 @@ class ESPNowPacket { esp_now_packet_type_t type() const { return this->type_; } const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; } - - private: - void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) { - memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN); - memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN); - memcpy(this->packet_.receive.data, data, size); - this->packet_.receive.size = size; - - this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi; - this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp; - - this->packet_.receive.info.rx_ctrl = reinterpret_cast(&this->packet_.receive.rx_ctrl); - } - - void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) { - memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN); - this->packet_.sent.status = status; - } }; class ESPNowSendPacket { @@ -151,9 +119,9 @@ class ESPNowSendPacket { ESPNowSendPacket(const ESPNowSendPacket &) = delete; ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete; - void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) { + void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, send_callback_t &&callback) { this->init_data_(peer_address, payload, size); - this->callback_ = callback; + this->callback_ = std::move(callback); } void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) { @@ -180,4 +148,4 @@ class ESPNowSendPacket { } // namespace esphome::espnow -#endif // USE_ESP32 +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/components/espnow/espnow_platform.h b/esphome/components/espnow/espnow_platform.h new file mode 100644 index 00000000000..bc19f34a7cd --- /dev/null +++ b/esphome/components/espnow/espnow_platform.h @@ -0,0 +1,34 @@ +#pragma once + +#if defined(USE_ESP32) || defined(USE_ESP8266) + +#include "espnow_types.h" + +#include + +// The radio and ESP-NOW driver calls that differ between ESP-IDF and the ESP8266 NONOS SDK. Each platform +// implements the whole set in its own source file, so the component itself carries no platform checks. +namespace esphome::espnow::platform { + +// Bring up the station interface when no wifi component owns the radio +void init_radio(); +void set_channel(uint8_t channel); +uint8_t get_channel(); +void read_mac(uint8_t *mac); + +// Start the driver and route its callbacks to the component +esp_err_t init(); +esp_err_t deinit(); +uint32_t get_version(); // 0 where the SDK has no version call + +bool peer_exists(const uint8_t *mac); +esp_err_t add_peer(const uint8_t *mac); +esp_err_t del_peer(const uint8_t *mac); +// Move the registered peers to the radio's current channel; only the NONOS SDK binds peers to a channel +void rebind_peers(); + +esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size); + +} // namespace esphome::espnow::platform + +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/components/espnow/espnow_platform_esp32.cpp b/esphome/components/espnow/espnow_platform_esp32.cpp new file mode 100644 index 00000000000..b45c09b072e --- /dev/null +++ b/esphome/components/espnow/espnow_platform_esp32.cpp @@ -0,0 +1,96 @@ +#include "espnow_platform.h" + +#ifdef USE_ESP32 + +#include "espnow_component.h" + +#include +#include +#include + +#include + +namespace esphome::espnow::platform { + +static void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { + global_esp_now->packet_received(info->src_addr, info->des_addr, data, size, info->rx_ctrl->rssi, + info->rx_ctrl->timestamp); +} + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +static void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) { + global_esp_now->send_reported(info->des_addr, status); +} +#else +static void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) { + global_esp_now->send_reported(mac_addr, status); +} +#endif + +void init_radio() { + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_ERROR_CHECK(esp_wifi_disconnect()); +} + +void set_channel(uint8_t channel) { + esp_wifi_set_promiscuous(true); + esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); + esp_wifi_set_promiscuous(false); +} + +uint8_t get_channel() { + uint8_t channel = 0; + wifi_second_chan_t second; + esp_wifi_get_channel(&channel, &second); + return channel; +} + +void read_mac(uint8_t *mac) { esp_wifi_get_mac(WIFI_IF_STA, mac); } + +esp_err_t init() { + esp_err_t err = esp_now_init(); + if (err == ESP_OK) { + err = esp_now_register_recv_cb(on_data_received); + } + if (err == ESP_OK) { + err = esp_now_register_send_cb(on_send_report); + } + return err; +} + +esp_err_t deinit() { + esp_now_unregister_recv_cb(); + esp_now_unregister_send_cb(); + return esp_now_deinit(); +} + +uint32_t get_version() { + uint32_t version = 0; + esp_now_get_version(&version); + return version; +} + +bool peer_exists(const uint8_t *mac) { return esp_now_is_peer_exist(mac); } + +esp_err_t add_peer(const uint8_t *mac) { + esp_now_peer_info_t peer_info = {}; + peer_info.ifidx = WIFI_IF_STA; + memcpy(peer_info.peer_addr, mac, ESP_NOW_ETH_ALEN); + return esp_now_add_peer(&peer_info); +} + +esp_err_t del_peer(const uint8_t *mac) { return esp_now_del_peer(mac); } + +// Peers are added on channel 0, which ESP-IDF treats as whatever channel the radio is on +void rebind_peers() {} + +esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size) { return esp_now_send(mac, data, size); } + +} // namespace esphome::espnow::platform + +#endif // USE_ESP32 diff --git a/esphome/components/espnow/espnow_platform_esp8266.cpp b/esphome/components/espnow/espnow_platform_esp8266.cpp new file mode 100644 index 00000000000..736e8788375 --- /dev/null +++ b/esphome/components/espnow/espnow_platform_esp8266.cpp @@ -0,0 +1,105 @@ +#include "espnow_platform.h" + +#ifdef USE_ESP8266 + +#include "espnow_component.h" + +namespace esphome::espnow::platform { + +// The SDK reports 0 for success and a non-zero value for any failure +static esp_err_t sdk_result(int result) { return result == 0 ? ESP_OK : ESP_FAIL; } + +// Stands in for the destination address, which the SDK does not report +static const uint8_t UNKNOWN_ADDR[ESP_NOW_ETH_ALEN] = {0}; + +static void on_data_received(uint8_t *mac_addr, uint8_t *data, uint8_t size) { + global_esp_now->packet_received(mac_addr, UNKNOWN_ADDR, data, size, 0, 0); +} + +static void on_send_report(uint8_t *mac_addr, uint8_t status) { + global_esp_now->send_reported(mac_addr, status == 0 ? ESP_NOW_SEND_SUCCESS : ESP_NOW_SEND_FAIL); +} + +void init_radio() { + // The NONOS SDK only delivers broadcast frames to ESP-NOW while the station is associated or the + // soft-AP interface is up. Without Wi-Fi, run a hidden, open soft-AP that accepts no clients so + // broadcasts (for example from a WizMote style remote) are received. + softap_config ap{}; + ap.channel = 1; + ap.authmode = AUTH_OPEN; + ap.ssid_hidden = 1; + ap.max_connection = 0; + ap.beacon_interval = 60000; + wifi_set_opmode_current(STATIONAP_MODE); + wifi_softap_set_config_current(&ap); + wifi_softap_dhcps_stop(); + wifi_set_sleep_type(NONE_SLEEP_T); + wifi_station_disconnect(); +} + +void set_channel(uint8_t channel) { + wifi_promiscuous_enable(true); + wifi_set_channel(channel); + wifi_promiscuous_enable(false); + // Keep the soft-AP from init_radio() on the same channel so the SDK never retunes the radio to its own + softap_config ap{}; + if (wifi_get_opmode() == STATIONAP_MODE && wifi_softap_get_config(&ap) && ap.channel != channel) { + ap.channel = channel; + wifi_softap_set_config_current(&ap); + } +} + +uint8_t get_channel() { return wifi_get_channel(); } + +void read_mac(uint8_t *mac) { + // Frames leave from the station interface unless the device runs as a soft-AP only + wifi_get_macaddr(wifi_get_opmode() == SOFTAP_MODE ? SOFTAP_IF : STATION_IF, mac); +} + +esp_err_t init() { + int result = esp_now_init(); + if (result == 0) { + // CONTROLLER sends from the station interface; the other roles send from the soft-AP MAC whenever + // that interface is up, so peers would no longer recognise the sender. + result = esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER); + } + if (result == 0) { + result = esp_now_register_recv_cb(on_data_received); + } + if (result == 0) { + result = esp_now_register_send_cb(on_send_report); + } + return sdk_result(result); +} + +esp_err_t deinit() { + esp_now_unregister_recv_cb(); + esp_now_unregister_send_cb(); + return sdk_result(esp_now_deinit()); +} + +uint32_t get_version() { return 0; } + +// The SDK returns 1 when the peer exists, 0 when it does not and a negative value on error +bool peer_exists(const uint8_t *mac) { return esp_now_is_peer_exist(const_cast(mac)) > 0; } + +esp_err_t add_peer(const uint8_t *mac) { + return sdk_result(esp_now_add_peer(const_cast(mac), ESP_NOW_ROLE_COMBO, wifi_get_channel(), nullptr, 0)); +} + +esp_err_t del_peer(const uint8_t *mac) { return sdk_result(esp_now_del_peer(const_cast(mac))); } + +void rebind_peers() { + const uint8_t channel = wifi_get_channel(); + for (uint8_t *mac = esp_now_fetch_peer(true); mac != nullptr; mac = esp_now_fetch_peer(false)) { + esp_now_set_peer_channel(mac, channel); + } +} + +esp_err_t send(const uint8_t *mac, const uint8_t *data, uint16_t size) { + return sdk_result(esp_now_send(const_cast(mac), const_cast(data), size)); +} + +} // namespace esphome::espnow::platform + +#endif // USE_ESP8266 diff --git a/esphome/components/espnow/espnow_types.h b/esphome/components/espnow/espnow_types.h new file mode 100644 index 00000000000..4b9c65fecf3 --- /dev/null +++ b/esphome/components/espnow/espnow_types.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#if defined(USE_ESP32) + +#include +#include + +#elif defined(USE_ESP8266) + +extern "C" { +#include +#include +#include +} + +#endif + +namespace esphome::espnow { + +struct WifiPacketRxControl { + int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm + uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or + // light sleep is not enabled +}; + +#if defined(USE_ESP32) + +// Handlers read rx_ctrl through the ESP-IDF type so existing lambdas keep compiling +using rx_ctrl_t = wifi_pkt_rx_ctrl_t; +static constexpr bool ESPNOW_REPORTS_DESTINATION = true; + +// Maximum size of the ESPNow event queues - must be a power of 2 for the lock-free queue +static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16; +static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16; + +#elif defined(USE_ESP8266) + +// The NONOS SDK has no esp_err_t vocabulary. These are the ESP-IDF names and values the shared espnow code +// uses, so it reads the same on both platforms. +using esp_err_t = int32_t; + +static constexpr esp_err_t ESP_OK = 0; +static constexpr esp_err_t ESP_FAIL = -1; +static constexpr esp_err_t ESP_ERR_INVALID_MAC = 0x10B; +static constexpr esp_err_t ESP_ERR_ESPNOW_BASE = 0x3064; +static constexpr esp_err_t ESP_ERR_ESPNOW_NOT_INIT = ESP_ERR_ESPNOW_BASE + 1; +static constexpr esp_err_t ESP_ERR_ESPNOW_ARG = ESP_ERR_ESPNOW_BASE + 2; +static constexpr esp_err_t ESP_ERR_ESPNOW_NO_MEM = ESP_ERR_ESPNOW_BASE + 3; +static constexpr esp_err_t ESP_ERR_ESPNOW_NOT_FOUND = ESP_ERR_ESPNOW_BASE + 5; +static constexpr esp_err_t ESP_ERR_ESPNOW_INTERNAL = ESP_ERR_ESPNOW_BASE + 6; +static constexpr esp_err_t ESP_ERR_ESPNOW_IF = ESP_ERR_ESPNOW_BASE + 8; + +inline const char *esp_err_to_name(esp_err_t err) { return err == ESP_OK ? "ESP_OK" : "ESP_FAIL"; } + +static constexpr uint8_t ESP_NOW_ETH_ALEN = 6; +static constexpr uint8_t ESP_NOW_MAX_DATA_LEN = 250; + +// NOLINTNEXTLINE(readability-identifier-naming) +enum esp_now_send_status_t : uint8_t { + ESP_NOW_SEND_SUCCESS = 0, + ESP_NOW_SEND_FAIL = 1, +}; + +using rx_ctrl_t = WifiPacketRxControl; +// The SDK reports neither the destination address nor the signal strength of a received frame +static constexpr bool ESPNOW_REPORTS_DESTINATION = false; + +// Half the ESP32 depth: the heap is far smaller and only one send is ever in flight +static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 8; +static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 8; + +#endif + +} // namespace esphome::espnow diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 7053229db42..93bc181a185 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -1,10 +1,12 @@ #include "espnow_transport.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP8266) #include "esphome/core/application.h" #include "esphome/core/log.h" +#include + namespace esphome::espnow { ESPHOME_LOG_TAG(TAG, "espnow.transport"); @@ -64,9 +66,7 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data return false; } - this->packet_buffer_.resize(size); - memcpy(this->packet_buffer_.data(), data, size); - this->process_(this->packet_buffer_); + this->process_(std::span(data, size)); return false; // Allow other handlers to run } @@ -79,12 +79,10 @@ bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *da return false; } - this->packet_buffer_.resize(size); - memcpy(this->packet_buffer_.data(), data, size); - this->process_(this->packet_buffer_); + this->process_(std::span(data, size)); return false; // Allow other handlers to run } } // namespace esphome::espnow -#endif // USE_ESP32 +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h index 51069b64153..79938f8df2e 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.h +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -2,7 +2,7 @@ #include "../espnow_component.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ESP8266) #include "esphome/core/component.h" #include "esphome/components/packet_transport/packet_transport.h" @@ -33,9 +33,8 @@ class ESPNowTransport final : public packet_transport::PacketTransport, bool should_send() override; peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}; - std::vector packet_buffer_; }; } // namespace esphome::espnow -#endif // USE_ESP32 +#endif // USE_ESP32 || USE_ESP8266 diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index b53b8064a3d..77b47228a97 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,7 @@ #pragma once -#if defined(USE_ESP32) || defined(USE_ZEPHYR) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_HOST) +#if defined(USE_ESP32) || defined(USE_ZEPHYR) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_HOST) || \ + defined(USE_ESP8266) #include #include diff --git a/tests/components/espnow/common-esp32.yaml b/tests/components/espnow/common-esp32.yaml new file mode 100644 index 00000000000..a97a5ef8aeb --- /dev/null +++ b/tests/components/espnow/common-esp32.yaml @@ -0,0 +1,9 @@ +espnow: + max_payload_size: 1470 + on_broadcast: + - logger.log: + format: "Broadcast from: %s = '%s' RSSI: %d" + args: + - format_mac_address_pretty(info.src_addr).c_str() + - format_hex_pretty(data, size).c_str() + - info.rx_ctrl->rssi diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index 973781fb68b..54b089116f7 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -2,7 +2,6 @@ espnow: id: espnow_component auto_add_peer: false channel: 1 - max_payload_size: 1470 peers: - 11:22:33:44:55:66 on_receive: @@ -50,13 +49,6 @@ espnow: - espnow.set_channel: 6 - espnow.set_channel: channel: !lambda 'return 6;' - on_broadcast: - - logger.log: - format: "Broadcast from: %s = '%s' RSSI: %d" - args: - - format_mac_address_pretty(info.src_addr).c_str() - - format_hex_pretty(data, size).c_str() - - info.rx_ctrl->rssi on_unknown_peer: - logger.log: format: "Unknown peer: %s = '%s' RSSI: %d" @@ -80,8 +72,9 @@ packet_transport: key: "0123456789abcdef0123456789abcdef" sensor: - - platform: internal_temperature + - platform: template id: espnow_temp_sensor + lambda: return 21.5; - platform: packet_transport transport_id: transport1 diff --git a/tests/components/espnow/test-wifi.esp8266-ard.yaml b/tests/components/espnow/test-wifi.esp8266-ard.yaml new file mode 100644 index 00000000000..c45547cd533 --- /dev/null +++ b/tests/components/espnow/test-wifi.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + espnow: !include common-wifi.yaml diff --git a/tests/components/espnow/test.esp32-idf.yaml b/tests/components/espnow/test.esp32-idf.yaml index b47e39c3898..1b3ba941b6b 100644 --- a/tests/components/espnow/test.esp32-idf.yaml +++ b/tests/components/espnow/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml - -<<: !include common.yaml + espnow: !include common.yaml + espnow_esp32: !include common-esp32.yaml diff --git a/tests/components/espnow/test.esp8266-ard.yaml b/tests/components/espnow/test.esp8266-ard.yaml new file mode 100644 index 00000000000..ea7d1f197e1 --- /dev/null +++ b/tests/components/espnow/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + espnow: !include common.yaml diff --git a/tests/unit_tests/components/test_espnow.py b/tests/unit_tests/components/test_espnow.py index 21305c2b332..762e660139c 100644 --- a/tests/unit_tests/components/test_espnow.py +++ b/tests/unit_tests/components/test_espnow.py @@ -7,16 +7,29 @@ from esphome.components.esp32.const import ( VARIANT_ESP32H2, VARIANT_ESP32P4, ) -from esphome.components.espnow import _validate_variant +from esphome.components.espnow import CONFIG_SCHEMA, _validate_variant import esphome.config_validation as cv +from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) +from esphome.core import CORE import esphome.final_validate as fv from esphome.types import ConfigType def _run( - monkeypatch, variant: str, full_config: dict, config: ConfigType + monkeypatch, + variant: str, + full_config: dict, + config: ConfigType, + platform: str = PLATFORM_ESP32, ) -> ConfigType: monkeypatch.setattr("esphome.components.espnow.get_esp32_variant", lambda: variant) + monkeypatch.setitem(CORE.data, KEY_CORE, {KEY_TARGET_PLATFORM: platform}) token = fv.full_config.set(full_config) try: return _validate_variant(config) @@ -46,3 +59,39 @@ def test_p4_with_esp32_hosted_passes(monkeypatch) -> None: """The P4 with esp32_hosted present validates; config passes through.""" config = {"id": "espnow"} assert _run(monkeypatch, VARIANT_ESP32P4, {"esp32_hosted": {}}, config) is config + + +def test_esp8266_skips_variant_check(monkeypatch) -> None: + """The ESP8266 has one Wi-Fi radio and no variants, so the check does not run.""" + config = {"id": "espnow"} + assert _run(monkeypatch, "", {}, config, platform=PLATFORM_ESP8266) is config + + +def _esp8266_arduino(monkeypatch) -> None: + monkeypatch.setitem( + CORE.data, + KEY_CORE, + {KEY_TARGET_PLATFORM: PLATFORM_ESP8266, KEY_TARGET_FRAMEWORK: "arduino"}, + ) + + +def test_esp8266_accepts_the_v1_payload_size(monkeypatch) -> None: + _esp8266_arduino(monkeypatch) + assert ( + CONFIG_SCHEMA({"channel": 1, "max_payload_size": 250})["max_payload_size"] + == 250 + ) + + +def test_esp8266_rejects_v2_payload_sizes(monkeypatch) -> None: + """The NONOS SDK only speaks ESP-NOW v1, so larger frames are refused at validation.""" + _esp8266_arduino(monkeypatch) + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({"channel": 1, "max_payload_size": 251}) + + +def test_esp8266_rejects_on_broadcast(monkeypatch) -> None: + """The NONOS SDK does not report a destination address, so broadcasts cannot be told apart.""" + _esp8266_arduino(monkeypatch) + with pytest.raises(cv.Invalid, match="only available on"): + CONFIG_SCHEMA({"channel": 1, "on_broadcast": []})