[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 <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Mathew Winstone
2026-10-11 03:45:03 +00:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] Copilot J. Nick Koston J. Nick Koston
parent fdf1752307
commit 6bc19d2a1a
19 changed files with 523 additions and 218 deletions
+24 -7
View File
@@ -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
+2 -2
View File
@@ -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<const ESPNowRecvInfo &, const ui
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+67 -100
View File
@@ -1,25 +1,19 @@
#include "espnow_component.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_err.h"
#include "espnow_platform.h"
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <utility>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_event.h>
#include <esp_mac.h>
#include <esp_netif.h>
#include <esp_now.h>
#include <esp_random.h>
#include <esp_wifi.h>
#include <cstring>
#include <memory>
#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_t>(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::span<const uint
if (this->wifi_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<uint16_t>(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
+12 -23
View File
@@ -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 <esp_idf_version.h>
#include <esp_mac.h>
#include <esp_now.h>
#include <array>
#include <map>
#include <memory>
@@ -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<uint8_t, ESP_NOW_ETH_ALEN>;
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<uint8_t> &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<uint8_t> &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
+9 -10
View File
@@ -1,19 +1,18 @@
#pragma once
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include <esp_err.h>
#include <esp_now.h>
#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
+19 -51
View File
@@ -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 <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
#include <esp_err.h>
#include <esp_idf_version.h>
#include <esp_now.h>
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<void(esp_err_t)>;
@@ -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<rx_ctrl_t *>(&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<wifi_pkt_rx_ctrl_t *>(&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
@@ -0,0 +1,34 @@
#pragma once
#if defined(USE_ESP32) || defined(USE_ESP8266)
#include "espnow_types.h"
#include <cstdint>
// 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
@@ -0,0 +1,96 @@
#include "espnow_platform.h"
#ifdef USE_ESP32
#include "espnow_component.h"
#include <esp_idf_version.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <cstring>
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
@@ -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<uint8_t *>(mac)) > 0; }
esp_err_t add_peer(const uint8_t *mac) {
return sdk_result(esp_now_add_peer(const_cast<uint8_t *>(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<uint8_t *>(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<uint8_t *>(mac), const_cast<uint8_t *>(data), size));
}
} // namespace esphome::espnow::platform
#endif // USE_ESP8266
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include <cstddef>
#include <cstdint>
#if defined(USE_ESP32)
#include <esp_err.h>
#include <esp_now.h>
#elif defined(USE_ESP8266)
extern "C" {
#include <c_types.h>
#include <espnow.h>
#include <user_interface.h>
}
#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
@@ -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 <span>
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<const uint8_t>(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<const uint8_t>(data, size));
return false; // Allow other handlers to run
}
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
@@ -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<uint8_t> packet_buffer_;
};
} // namespace esphome::espnow
#endif // USE_ESP32
#endif // USE_ESP32 || USE_ESP8266
+2 -1
View File
@@ -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 <atomic>
#include <cstddef>
@@ -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
+2 -9
View File
@@ -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
@@ -0,0 +1,2 @@
packages:
espnow: !include common-wifi.yaml
+2 -2
View File
@@ -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
@@ -0,0 +1,2 @@
packages:
espnow: !include common.yaml
+51 -2
View File
@@ -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": []})