diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 13f278d3bc..c6c90ed67a 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -41,7 +41,7 @@ DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action) ESPNowHandlerTrigger = automation.Trigger.template( ESPNowRecvInfoConstRef, cg.uint8.operator("const").operator("ptr"), - cg.uint8, + cg.uint16, ) OnUnknownPeerTrigger = espnow_ns.class_( @@ -56,6 +56,20 @@ OnBroadcastTrigger = espnow_ns.class_( CONF_AUTO_ADD_PEER = "auto_add_peer" +CONF_MAX_PAYLOAD_SIZE = "max_payload_size" + +# Payload limits of ESP-NOW v1 and v2 frames. The radio negotiates the +# protocol version per peer on its own; the option only sizes this device's +# packet buffers, whose static RAM cost is proportional to it (~8 KB at 250 +# bytes, ~44 KB at 1470). +ESPNOW_PAYLOAD_V1 = 250 +ESPNOW_PAYLOAD_V2 = 1470 + +# Config-time cap for action payloads. The per-device limit is the +# ``max_payload_size`` option, which the action schema cannot see; send() +# enforces it at runtime. +MAX_ESPNOW_PACKET_SIZE = ESPNOW_PAYLOAD_V2 + CONF_PEERS = "peers" CONF_ON_SENT = "on_sent" CONF_ON_UNKNOWN_PEER = "on_unknown_peer" @@ -63,7 +77,15 @@ CONF_ON_BROADCAST = "on_broadcast" CONF_CONTINUE_ON_ERROR = "continue_on_error" CONF_WAIT_FOR_SENT = "wait_for_sent" -MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes + +def _validate_max_payload_size(value: int) -> int: + if value > ESPNOW_PAYLOAD_V1: + return cv.require_framework_version( + esp_idf=cv.Version(5, 4, 0), + esp32_arduino=cv.Version(3, 2, 0), + extra_message="ESP-NOW v2 frames need an ESP-NOW v2 capable framework", + )(value) + return value def validate_channel(value): @@ -78,6 +100,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(ESPNowComponent), cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional(CONF_MAX_PAYLOAD_SIZE, default=ESPNOW_PAYLOAD_V1): cv.All( + cv.int_range(min=1, max=ESPNOW_PAYLOAD_V2), _validate_max_payload_size + ), cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean, cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address), cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation( @@ -113,7 +138,7 @@ async def _trigger_to_code(config): [ (ESPNowRecvInfoConstRef, "info"), (cg.uint8.operator("const").operator("ptr"), "data"), - (cg.uint8, "size"), + (cg.uint16, "size"), ], config, ) @@ -125,6 +150,7 @@ async def to_code(config): await cg.register_component(var, config) cg.add_define("USE_ESPNOW") + cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE]) if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 5e995aff53..e4d01bb1a8 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -119,7 +119,7 @@ template class SetChannelAction final : public Action, pu } }; -class OnReceiveTrigger final : public Trigger, +class OnReceiveTrigger final : public Trigger, public ESPNowReceivedPacketHandler { public: explicit OnReceiveTrigger(std::array address) : has_address_(true) { @@ -128,7 +128,7 @@ class OnReceiveTrigger final : public Triggerhas_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); if (!match) return false; @@ -141,15 +141,15 @@ class OnReceiveTrigger final : public Trigger, +class OnUnknownPeerTrigger final : public Trigger, public ESPNowUnknownPeerHandler { public: - bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override { this->trigger(info, data, size); return false; // Return false to continue processing other internal handlers } }; -class OnBroadcastTrigger final : public Trigger, +class OnBroadcastTrigger final : public Trigger, public ESPNowBroadcastHandler { public: explicit OnBroadcastTrigger(std::array address) : has_address_(true) { @@ -157,7 +157,7 @@ class OnBroadcastTrigger final : public Triggerhas_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); if (!match) return false; diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 91f2c067ca..f28d7f3354 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,7 @@ #include "espnow_err.h" +#include #include #include "esphome/core/application.h" @@ -96,9 +97,9 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { // 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 our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger - // frame would overflow packet_.receive.data. - if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) { + // but the receive buffer only fits v2 frames with ``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(); return; } @@ -285,11 +286,14 @@ void ESPNowComponent::loop() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + // Cap the hex dump at a v1 frame: a full v2 frame would need a + // ~4.4 KB stack buffer. char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)]; format_mac_addr_upper(info.src_addr, src_buf); format_mac_addr_upper(info.des_addr, dst_buf); ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf, - format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size)); + 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) { for (auto *handler : this->broadcast_handlers_) { @@ -362,7 +366,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl 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 > ESP_NOW_MAX_DATA_LEN) { + } else if (size > ESPNOW_MAX_DATA_LEN) { return ESP_ERR_ESPNOW_DATA_SIZE; } else if (!esp_now_is_peer_exist(peer_address)) { if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) { diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index eacc3eb886..d95255c5df 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -62,7 +62,7 @@ class ESPNowUnknownPeerHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0; }; /// Handler interface for receiving ESPNow packets @@ -74,7 +74,7 @@ class ESPNowReceivedPacketHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0; }; /// Handler interface for receiving ESPNow broadcast packets /// Components should inherit from this class to handle incoming ESPNow data @@ -85,7 +85,7 @@ class ESPNowBroadcastHandler { /// @param data Pointer to the received data payload /// @param size Size of the received data in bytes /// @return true if the packet was handled, false otherwise - virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; + virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0; }; class ESPNowComponent final : public Component { diff --git a/esphome/components/espnow/espnow_packet.h b/esphome/components/espnow/espnow_packet.h index b6192a0d41..fb125864fb 100644 --- a/esphome/components/espnow/espnow_packet.h +++ b/esphome/components/espnow/espnow_packet.h @@ -19,6 +19,23 @@ namespace esphome::espnow { static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE}; +// Maximum payload this component sends and receives, from the +// ``max_payload_size`` option. The radio stack speaks ESP-NOW v2 regardless +// (negotiated per peer); payloads beyond the v1 limit (250 bytes) are opt-in +// because the packet pools are statically sized from this, so their RAM cost +// is proportional (~8 KB at 250 bytes, ~44 KB at the v2 limit of 1470). +#ifndef USE_ESPNOW_MAX_PAYLOAD_SIZE +#define USE_ESPNOW_MAX_PAYLOAD_SIZE ESP_NOW_MAX_DATA_LEN +#endif +static constexpr uint16_t ESPNOW_MAX_DATA_LEN = USE_ESPNOW_MAX_PAYLOAD_SIZE; +#ifdef ESP_NOW_MAX_DATA_LEN_V2 +static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN_V2, + "espnow max_payload_size cannot exceed the ESP-NOW v2 frame limit"); +#else +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 @@ -78,10 +95,10 @@ class ESPNowPacket { union { // NOLINTNEXTLINE(readability-identifier-naming) struct received_data { - ESPNowRecvInfo info; // Information about the received packet - uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet - uint8_t size; // Size of the received data - WifiPacketRxControl rx_ctrl; // Status of the received packet + ESPNowRecvInfo info; // Information about the received packet + uint8_t data[ESPNOW_MAX_DATA_LEN]; // Data received in the packet + uint16_t size; // Size of the received data + WifiPacketRxControl rx_ctrl; // Status of the received packet } receive; // NOLINTNEXTLINE(readability-identifier-naming) @@ -144,15 +161,15 @@ class ESPNowSendPacket { this->callback_ = nullptr; // Reset callback } - uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to - uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send - uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN - send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete + uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to + uint8_t data_[ESPNOW_MAX_DATA_LEN]{0}; // Data to send + uint16_t size_{0}; // Size of the data to send, must be <= ESPNOW_MAX_DATA_LEN + send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete private: void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) { memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN); - if (size > ESP_NOW_MAX_DATA_LEN) { + if (size > ESPNOW_MAX_DATA_LEN) { this->size_ = 0; return; } diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 1e37073321..b7686f23d6 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -42,8 +42,8 @@ void ESPNowTransport::send_packet(const std::vector &buf) const { return; } - if (buf.size() > ESP_NOW_MAX_DATA_LEN) { - ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN); + if (buf.size() > ESPNOW_MAX_DATA_LEN) { + ESP_LOGE(TAG, "Packet too large: %zu bytes (max %u)", buf.size(), (unsigned) ESPNOW_MAX_DATA_LEN); return; } @@ -55,8 +55,8 @@ void ESPNowTransport::send_packet(const std::vector &buf) const { }); } -bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { - ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], +bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) { + ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); if (data == nullptr || size == 0) { @@ -70,9 +70,9 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data return false; // Allow other handlers to run } -bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) { - ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0], - info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); +bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) { + ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, + info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]); if (data == nullptr || size == 0) { ESP_LOGW(TAG, "Received empty or null broadcast packet"); diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h index 7e1d08618b..51069b6415 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.h +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -24,12 +24,12 @@ class ESPNowTransport final : public packet_transport::PacketTransport, } // ESPNow handler interface - bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; - bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override; + bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override; + bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override; protected: void send_packet(const std::vector &buf) const override; - size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; } + size_t get_max_packet_size() override { return ESPNOW_MAX_DATA_LEN; } bool should_send() override; peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 3e8b0829c5..cdc26c9222 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -242,6 +242,8 @@ #define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_ESP32_RTC_PREFERENCES #define USE_ESP32_SRAM1_AS_IRAM +#define USE_ESPNOW +#define USE_ESPNOW_MAX_PAYLOAD_SIZE 1470 #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index f05735e8f4..ae43baa41a 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -2,6 +2,7 @@ espnow: id: espnow_component auto_add_peer: false channel: 1 + max_payload_size: 1470 peers: - 11:22:33:44:55:66 on_receive: