[espnow] Basic communication between ESP32 devices (#9582)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
NP v/d Spek 2025-08-01 06:51:01 +02:00 committed by GitHub
parent 291215909a
commit c42c5dd946
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1384 additions and 0 deletions

View File

@ -155,6 +155,7 @@ esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito

View File

@ -0,0 +1,320 @@
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import wifi
from esphome.components.udp import CONF_ON_RECEIVE
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_CHANNEL,
CONF_DATA,
CONF_ENABLE_ON_BOOT,
CONF_ID,
CONF_ON_ERROR,
CONF_TRIGGER_ID,
CONF_WIFI,
)
from esphome.core import CORE, HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
byte_vector = cg.std_vector.template(cg.uint8)
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
espnow_ns = cg.esphome_ns.namespace("espnow")
ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component)
# Handler interfaces that other components can use to register callbacks
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
SendAction = espnow_ns.class_("SendAction", automation.Action)
SetChannelAction = espnow_ns.class_("SetChannelAction", automation.Action)
AddPeerAction = espnow_ns.class_("AddPeerAction", automation.Action)
DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
ESPNowHandlerTrigger = automation.Trigger.template(
ESPNowRecvInfoConstRef,
cg.uint8.operator("const").operator("ptr"),
cg.uint8,
)
OnUnknownPeerTrigger = espnow_ns.class_(
"OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler
)
OnReceiveTrigger = espnow_ns.class_(
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
)
OnBroadcastedTrigger = espnow_ns.class_(
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
)
CONF_AUTO_ADD_PEER = "auto_add_peer"
CONF_PEERS = "peers"
CONF_ON_SENT = "on_sent"
CONF_ON_UNKNOWN_PEER = "on_unknown_peer"
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_unknown_peer(config):
if config[CONF_AUTO_ADD_PEER] and config.get(CONF_ON_UNKNOWN_PEER):
raise cv.Invalid(
f"'{CONF_ON_UNKNOWN_PEER}' cannot be used when '{CONF_AUTO_ADD_PEER}' is enabled.",
path=[CONF_ON_UNKNOWN_PEER],
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPNowComponent),
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
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(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnUnknownPeerTrigger),
},
single=True,
),
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnReceiveTrigger),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
},
).extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
_validate_unknown_peer,
)
async def _trigger_to_code(config):
if address := config.get(CONF_ADDRESS):
address = address.parts
trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], address)
await automation.build_automation(
trigger,
[
(ESPNowRecvInfoConstRef, "info"),
(cg.uint8.operator("const").operator("ptr"), "data"),
(cg.uint8, "size"),
],
config,
)
return trigger
async def to_code(config):
print(config)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CORE.using_arduino:
cg.add_library("WiFi", None)
cg.add_define("USE_ESPNOW")
if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel))
cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER]))
for peer in config.get(CONF_PEERS, []):
cg.add(var.add_peer(peer.parts))
if on_receive := config.get(CONF_ON_UNKNOWN_PEER):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_unknown_peer_handler(trigger))
for on_receive in config.get(CONF_ON_RECEIVE, []):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_received_handler(trigger))
for on_receive in config.get(CONF_ON_BROADCAST, []):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_broadcasted_handler(trigger))
# ========================================== A C T I O N S ================================================
def validate_peer(value):
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
return cv.mac_address(value)
def _validate_raw_data(value):
if isinstance(value, str):
if len(value) >= MAX_ESPNOW_PACKET_SIZE:
raise cv.Invalid(
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
)
return value
if isinstance(value, list):
if len(value) > MAX_ESPNOW_PACKET_SIZE:
raise cv.Invalid(
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
)
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
f"'{CONF_DATA}' must either be a string wrapped in quotes or a list of bytes"
)
async def register_peer(var, config, args):
peer = config[CONF_ADDRESS]
if isinstance(peer, core.MACAddress):
peer = [HexInt(p) for p in peer.parts]
template_ = await cg.templatable(peer, args, peer_address_t, peer_address_t)
cg.add(var.set_address(template_))
PEER_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(ESPNowComponent),
cv.Required(CONF_ADDRESS): cv.templatable(cv.mac_address),
}
)
SEND_SCHEMA = PEER_SCHEMA.extend(
{
cv.Required(CONF_DATA): cv.templatable(_validate_raw_data),
cv.Optional(CONF_ON_SENT): automation.validate_action_list,
cv.Optional(CONF_ON_ERROR): automation.validate_action_list,
cv.Optional(CONF_WAIT_FOR_SENT, default=True): cv.boolean,
cv.Optional(CONF_CONTINUE_ON_ERROR, default=True): cv.boolean,
}
)
def _validate_send_action(config):
if not config[CONF_WAIT_FOR_SENT] and not config[CONF_CONTINUE_ON_ERROR]:
raise cv.Invalid(
f"'{CONF_CONTINUE_ON_ERROR}' cannot be false if '{CONF_WAIT_FOR_SENT}' is false as the automation will not wait for the failed result.",
path=[CONF_CONTINUE_ON_ERROR],
)
return config
SEND_SCHEMA.add_extra(_validate_send_action)
@automation.register_action(
"espnow.send",
SendAction,
SEND_SCHEMA,
)
@automation.register_action(
"espnow.broadcast",
SendAction,
cv.maybe_simple_value(
SEND_SCHEMA.extend(
{
cv.Optional(CONF_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
}
),
key=CONF_DATA,
),
)
async def send_action(
config: ConfigType,
action_id: core.ID,
template_arg: cg.TemplateArguments,
args: list[tuple],
):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await register_peer(var, config, args)
data = config.get(CONF_DATA, [])
if isinstance(data, str):
data = [cg.RawExpression(f"'{c}'") for c in data]
templ = await cg.templatable(data, args, byte_vector, byte_vector)
cg.add(var.set_data(templ))
cg.add(var.set_wait_for_sent(config[CONF_WAIT_FOR_SENT]))
cg.add(var.set_continue_on_error(config[CONF_CONTINUE_ON_ERROR]))
if on_sent_config := config.get(CONF_ON_SENT):
actions = await automation.build_action_list(on_sent_config, template_arg, args)
cg.add(var.add_on_sent(actions))
if on_error_config := config.get(CONF_ON_ERROR):
actions = await automation.build_action_list(
on_error_config, template_arg, args
)
cg.add(var.add_on_error(actions))
return var
@automation.register_action(
"espnow.peer.add",
AddPeerAction,
cv.maybe_simple_value(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
)
@automation.register_action(
"espnow.peer.delete",
DeletePeerAction,
cv.maybe_simple_value(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
)
async def peer_action(
config: ConfigType,
action_id: core.ID,
template_arg: cg.TemplateArguments,
args: list[tuple],
):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
await register_peer(var, config, args)
return var
@automation.register_action(
"espnow.set_channel",
SetChannelAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(ESPNowComponent),
cv.Required(CONF_CHANNEL): cv.templatable(wifi.validate_channel),
},
key=CONF_CHANNEL,
),
)
async def channel_action(
config: ConfigType,
action_id: core.ID,
template_arg: cg.TemplateArguments,
args: list[tuple],
):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
cg.add(var.set_channel(template_))
return var

View File

@ -0,0 +1,175 @@
#pragma once
#ifdef USE_ESP32
#include "espnow_component.h"
#include "esphome/core/automation.h"
#include "esphome/core/base_automation.h"
namespace esphome::espnow {
template<typename... Ts> class SendAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
public:
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
this->sent_.add_actions(actions);
if (this->flags_.wait_for_sent) {
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
}
}
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
this->error_.add_actions(actions);
if (this->flags_.wait_for_sent) {
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
if (this->flags_.continue_on_error) {
this->play_next_(x...);
} else {
this->stop_complex();
}
}));
}
}
void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; }
void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; }
void play_complex(Ts... x) override {
this->num_running_++;
send_callback_t send_callback = [this, x...](esp_err_t status) {
if (status == ESP_OK) {
if (this->sent_.empty() && this->flags_.wait_for_sent) {
this->play_next_(x...);
} else if (!this->sent_.empty()) {
this->sent_.play(x...);
}
} else {
if (this->error_.empty() && this->flags_.wait_for_sent) {
if (this->flags_.continue_on_error) {
this->play_next_(x...);
} else {
this->stop_complex();
}
} else if (!this->error_.empty()) {
this->error_.play(x...);
}
}
};
peer_address_t address = this->address_.value(x...);
std::vector<uint8_t> data = this->data_.value(x...);
esp_err_t err = this->parent_->send(address.data(), data, send_callback);
if (err != ESP_OK) {
send_callback(err);
} else if (!this->flags_.wait_for_sent) {
this->play_next_(x...);
}
}
void play(Ts... x) override { /* ignore - see play_complex */
}
void stop() override {
this->sent_.stop();
this->error_.stop();
}
protected:
ActionList<Ts...> sent_;
ActionList<Ts...> error_;
struct {
uint8_t wait_for_sent : 1; // Wait for the send operation to complete before continuing automation
uint8_t continue_on_error : 1; // Continue automation even if the send operation fails
uint8_t reserved : 6; // Reserved for future use
} flags_{0};
};
template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
public:
void play(Ts... x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->add_peer(address.data());
}
};
template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
public:
void play(Ts... x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->del_peer(address.data());
}
};
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
public:
TEMPLATABLE_VALUE(uint8_t, channel)
void play(Ts... x) override {
if (this->parent_->is_wifi_enabled()) {
return;
}
this->parent_->set_wifi_channel(this->channel_.value(x...));
this->parent_->apply_wifi_channel();
}
};
class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowReceivedPacketHandler {
public:
explicit OnReceiveTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
}
explicit OnReceiveTrigger() : has_address_(false) {}
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
this->trigger(info, data, size);
return false; // Return false to continue processing other internal handlers
}
protected:
bool has_address_{false};
const uint8_t *address_[ESP_NOW_ETH_ALEN];
};
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler {
public:
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
this->trigger(info, data, size);
return false; // Return false to continue processing other internal handlers
}
};
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastedHandler {
public:
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
}
explicit OnBroadcastedTrigger() : has_address_(false) {}
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
this->trigger(info, data, size);
return false; // Return false to continue processing other internal handlers
}
protected:
bool has_address_{false};
const uint8_t *address_[ESP_NOW_ETH_ALEN];
};
} // namespace esphome::espnow
#endif // USE_ESP32

View File

@ -0,0 +1,468 @@
#include "espnow_component.h"
#ifdef USE_ESP32
#include "espnow_err.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <esp_event.h>
#include <esp_mac.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
namespace esphome::espnow {
static constexpr const char *TAG = "espnow";
static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50;
static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100;
ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static const LogString *espnow_error_to_str(esp_err_t error) {
switch (error) {
case ESP_ERR_ESPNOW_FAILED:
return LOG_STR("ESPNow is in fail mode");
case ESP_ERR_ESPNOW_OWN_ADDRESS:
return LOG_STR("Message to your self");
case ESP_ERR_ESPNOW_DATA_SIZE:
return LOG_STR("Data size to large");
case ESP_ERR_ESPNOW_PEER_NOT_SET:
return LOG_STR("Peer address not set");
case ESP_ERR_ESPNOW_PEER_NOT_PAIRED:
return LOG_STR("Peer address not paired");
case ESP_ERR_ESPNOW_NOT_INIT:
return LOG_STR("Not init");
case ESP_ERR_ESPNOW_ARG:
return LOG_STR("Invalid argument");
case ESP_ERR_ESPNOW_INTERNAL:
return LOG_STR("Internal Error");
case ESP_ERR_ESPNOW_NO_MEM:
return LOG_STR("Our of memory");
case ESP_ERR_ESPNOW_NOT_FOUND:
return LOG_STR("Peer not found");
case ESP_ERR_ESPNOW_IF:
return LOG_STR("Interface does not match");
case ESP_OK:
return LOG_STR("OK");
case ESP_NOW_SEND_FAIL:
return LOG_STR("Failed");
default:
return LOG_STR("Unknown Error");
}
}
std::string peer_str(uint8_t *peer) {
if (peer == nullptr || peer[0] == 0) {
return "[Not Set]";
} else if (memcmp(peer, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
return "[Broadcast]";
} else if (memcmp(peer, ESPNOW_MULTICAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
return "[Multicast]";
} else {
return format_mac_address_pretty(peer);
}
}
#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
{
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->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();
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 because we're the only producer and the pool ensures we never exceed queue size
}
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->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();
return;
}
// Load new packet data (replaces previous packet)
packet->load_received_data(info, data, size);
// Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size
}
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
void ESPNowComponent::dump_config() {
uint32_t version = 0;
esp_now_get_version(&version);
ESP_LOGCONFIG(TAG, "espnow:");
if (this->is_disabled()) {
ESP_LOGCONFIG(TAG, " Disabled");
return;
}
ESP_LOGCONFIG(TAG,
" Own address: %s\n"
" Version: v%" PRIu32 "\n"
" Wi-Fi channel: %d",
format_mac_address_pretty(this->own_address_).c_str(), version, this->wifi_channel_);
#ifdef USE_WIFI
ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled()));
#endif
}
bool ESPNowComponent::is_wifi_enabled() {
#ifdef USE_WIFI
return wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled();
#else
return false;
#endif
}
void ESPNowComponent::setup() {
if (this->enable_on_boot_) {
this->enable_();
} else {
this->state_ = ESPNOW_STATE_DISABLED;
}
}
void ESPNowComponent::enable() {
if (this->state_ != ESPNOW_STATE_ENABLED)
return;
ESP_LOGD(TAG, "Enabling");
this->state_ = ESPNOW_STATE_OFF;
this->enable_();
}
void ESPNowComponent::enable_() {
if (!this->is_wifi_enabled()) {
esp_event_loop_create_default();
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());
this->apply_wifi_channel();
}
#ifdef USE_WIFI
else {
this->wifi_channel_ = wifi::global_wifi_component->get_wifi_channel();
}
#endif
esp_err_t err = esp_now_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_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_);
#ifdef USE_DEEP_SLEEP
esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW);
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
#endif
for (auto peer : this->peers_) {
this->add_peer(peer.address);
}
this->state_ = ESPNOW_STATE_ENABLED;
}
void ESPNowComponent::disable() {
if (this->state_ == ESPNOW_STATE_DISABLED)
return;
ESP_LOGD(TAG, "Disabling");
this->state_ = ESPNOW_STATE_DISABLED;
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
for (auto peer : this->peers_) {
this->del_peer(peer.address);
}
esp_err_t err = esp_now_deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
}
}
void ESPNowComponent::apply_wifi_channel() {
if (this->state_ == ESPNOW_STATE_DISABLED) {
ESP_LOGE(TAG, "Cannot set channel when ESPNOW disabled");
this->mark_failed();
return;
}
if (this->is_wifi_enabled()) {
ESP_LOGE(TAG, "Cannot set channel when Wi-Fi enabled");
this->mark_failed();
return;
}
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);
}
void ESPNowComponent::loop() {
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) {
int32_t new_channel = wifi::global_wifi_component->get_wifi_channel();
if (new_channel != this->wifi_channel_) {
ESP_LOGI(TAG, "Wifi Channel is changed from %d to %d.", this->wifi_channel_, new_channel);
this->wifi_channel_ = new_channel;
}
}
#endif
// Process received packets
ESPNowPacket *packet = this->receive_packet_queue_.pop();
while (packet != nullptr) {
switch (packet->type_) {
case ESPNowPacket::RECEIVED: {
const ESPNowRecvInfo info = packet->get_receive_info();
if (!esp_now_is_peer_exist(info.src_addr)) {
if (this->auto_add_peer_) {
this->add_peer(info.src_addr);
} else {
for (auto *handler : this->unknown_peer_handlers_) {
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
}
}
// Intentionally left as if instead of else in case the peer is added above
if (esp_now_is_peer_exist(info.src_addr)) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(),
format_mac_address_pretty(info.des_addr).c_str(),
format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str());
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcasted_handlers_) {
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
} else {
for (auto *handler : this->received_handlers_) {
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
}
}
break;
}
case ESPNowPacket::SENT: {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, ">>> [%s] %s", format_mac_address_pretty(packet->packet_.sent.address).c_str(),
LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
#endif
if (this->current_send_packet_ != nullptr) {
this->current_send_packet_->callback_(packet->packet_.sent.status);
this->send_packet_pool_.release(this->current_send_packet_);
this->current_send_packet_ = nullptr; // Reset current packet after sending
}
break;
}
default:
break;
}
// Return the packet to the pool
this->receive_packet_pool_.release(packet);
packet = this->receive_packet_queue_.pop();
}
// Process sending packet queue
if (this->current_send_packet_ == nullptr) {
this->send_();
}
// Log dropped received packets periodically
uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count();
if (received_dropped > 0) {
ESP_LOGW(TAG, "Dropped %u received packets due to buffer overflow", received_dropped);
}
// Log dropped send packets periodically
uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count();
if (send_dropped > 0) {
ESP_LOGW(TAG, "Dropped %u send packets due to buffer overflow", send_dropped);
}
}
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const 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) {
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) {
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_) {
esp_err_t err = this->add_peer(peer_address);
if (err != ESP_OK) {
return err;
}
} else {
return ESP_ERR_ESPNOW_PEER_NOT_PAIRED;
}
}
// Allocate a packet from the pool
ESPNowSendPacket *packet = this->send_packet_pool_.allocate();
if (packet == nullptr) {
this->send_packet_queue_.increment_dropped_count();
ESP_LOGE(TAG, "Failed to allocate send packet from pool");
this->status_momentary_warning("send-packet-pool-full");
return ESP_ERR_ESPNOW_NO_MEM;
}
// Load the packet data
packet->load_data(peer_address, payload, size, callback);
// Push the packet to the send queue
this->send_packet_queue_.push(packet);
return ESP_OK;
}
void ESPNowComponent::send_() {
ESPNowSendPacket *packet = this->send_packet_queue_.pop();
if (packet == nullptr) {
return; // No packets to send
}
this->current_send_packet_ = packet;
esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send packet to %s - %s", format_mac_address_pretty(packet->address_).c_str(),
LOG_STR_ARG(espnow_error_to_str(err)));
if (packet->callback_ != nullptr) {
packet->callback_(err);
}
this->status_momentary_warning("send-failed");
this->send_packet_pool_.release(packet);
this->current_send_packet_ = nullptr; // Reset current packet
return;
}
}
esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
return ESP_ERR_ESPNOW_NOT_INIT;
}
if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
this->mark_failed();
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 (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to add peer %s - %s", format_mac_address_pretty(peer).c_str(),
LOG_STR_ARG(espnow_error_to_str(err)));
this->status_momentary_warning("peer-add-failed");
return err;
}
}
bool found = false;
for (auto &it : this->peers_) {
if (it == peer) {
found = true;
break;
}
}
if (!found) {
ESPNowPeer new_peer;
memcpy(new_peer.address, peer, ESP_NOW_ETH_ALEN);
this->peers_.push_back(new_peer);
}
return ESP_OK;
}
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 (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to delete peer %s - %s", format_mac_address_pretty(peer).c_str(),
LOG_STR_ARG(espnow_error_to_str(err)));
this->status_momentary_warning("peer-del-failed");
return err;
}
}
for (auto it = this->peers_.begin(); it != this->peers_.end(); ++it) {
if (*it == peer) {
this->peers_.erase(it);
break;
}
}
return ESP_OK;
}
} // namespace esphome::espnow
#endif // USE_ESP32

View File

@ -0,0 +1,182 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32
#include "esphome/core/event_pool.h"
#include "esphome/core/lock_free_queue.h"
#include "espnow_packet.h"
#include <esp_idf_version.h>
#include <esp_mac.h>
#include <esp_now.h>
#include <array>
#include <map>
#include <memory>
#include <string>
#include <vector>
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 {
TRIGGER_NONE = 0,
ON_NEW_PEER = 1,
ON_RECEIVED = 2,
ON_BROADCASTED = 3,
ON_SUCCEED = 10,
ON_FAILED = 11,
};
enum ESPNowState : uint8_t {
/** Nothing has been initialized yet. */
ESPNOW_STATE_OFF = 0,
/** ESPNOW is disabled. */
ESPNOW_STATE_DISABLED,
/** ESPNOW is enabled. */
ESPNOW_STATE_ENABLED,
};
struct ESPNowPeer {
uint8_t address[ESP_NOW_ETH_ALEN]; // MAC address of the peer
bool operator==(const ESPNowPeer &other) const { return memcmp(this->address, other.address, ESP_NOW_ETH_ALEN) == 0; }
bool operator==(const uint8_t *other) const { return memcmp(this->address, other, ESP_NOW_ETH_ALEN) == 0; }
};
/// Handler interface for receiving ESPNow packets from unknown peers
/// Components should inherit from this class to handle incoming ESPNow data
class ESPNowUnknownPeerHandler {
public:
/// Called when an ESPNow packet is received from an unknown peer
/// @param info Information about the received packet (sender MAC, etc.)
/// @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;
};
/// Handler interface for receiving ESPNow packets
/// Components should inherit from this class to handle incoming ESPNow data
class ESPNowReceivedPacketHandler {
public:
/// Called when an ESPNow packet is received
/// @param info Information about the received packet (sender MAC, etc.)
/// @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_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving broadcasted ESPNow packets
/// Components should inherit from this class to handle incoming ESPNow data
class ESPNowBroadcastedHandler {
public:
/// Called when a broadcasted ESPNow packet is received
/// @param info Information about the received packet (sender MAC, etc.)
/// @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_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
class ESPNowComponent : public Component {
public:
ESPNowComponent();
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::LATE; }
// Add a peer to the internal list of peers
void add_peer(peer_address_t address) {
ESPNowPeer peer;
memcpy(peer.address, address.data(), ESP_NOW_ETH_ALEN);
this->peers_.push_back(peer);
}
// Add a peer with the esp_now api and add to the internal list if doesnt exist already
esp_err_t add_peer(const uint8_t *peer);
// Remove a peer with the esp_now api and remove from the internal list if exists
esp_err_t del_peer(const uint8_t *peer);
void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; }
void apply_wifi_channel();
void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; }
void enable();
void disable();
bool is_disabled() const { return this->state_ == ESPNOW_STATE_DISABLED; };
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
bool is_wifi_enabled();
/// @brief Queue a packet to be sent to a specific peer address.
/// This method will add the packet to the internal queue and
/// call the callback when the packet is sent.
/// Only one packet will be sent at any given time and the next one will not be sent until
/// the previous one has been acknowledged or failed.
/// @param peer_address MAC address of the peer to send the packet to
/// @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 uint8_t *payload, size_t size,
const send_callback_t &callback = nullptr);
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
this->unknown_peer_handlers_.push_back(handler);
}
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
this->broadcasted_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
void enable_();
void send_();
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
std::vector<ESPNowPeer> peers_{};
uint8_t own_address_[ESP_NOW_ETH_ALEN]{0};
LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{};
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{};
LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{};
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{};
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
uint8_t wifi_channel_{0};
ESPNowState state_{ESPNOW_STATE_OFF};
bool auto_add_peer_{false};
bool enable_on_boot_{true};
};
extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::espnow
#endif // USE_ESP32

View File

@ -0,0 +1,19 @@
#pragma once
#ifdef USE_ESP32
#include <esp_err.h>
#include <esp_now.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);
} // namespace esphome::espnow
#endif // USE_ESP32

View File

@ -0,0 +1,166 @@
#pragma once
#ifdef USE_ESP32
#include "espnow_err.h"
#include <cstdint>
#include <cstring>
#include <functional>
#include <memory>
#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};
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
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 */
};
using send_callback_t = std::function<void(esp_err_t)>;
class ESPNowPacket {
public:
// NOLINTNEXTLINE(readability-identifier-naming)
enum esp_now_packet_type_t : uint8_t {
RECEIVED,
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) {
this->type_ = RECEIVED;
this->init_received_data_(info, data, size);
}
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);
}
// Disable copy to prevent double-delete
ESPNowPacket(const ESPNowPacket &) = delete;
ESPNowPacket &operator=(const ESPNowPacket &) = delete;
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
} receive;
// NOLINTNEXTLINE(readability-identifier-naming)
struct sent_data {
uint8_t address[ESP_NOW_ETH_ALEN];
esp_now_send_status_t status;
} sent;
} packet_;
esp_now_packet_type_t type_;
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 {
public:
ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &&callback)
: callback_(callback) {
this->init_data_(peer_address, payload, size);
}
ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
this->init_data_(peer_address, payload, size);
}
// Default constructor for pre-allocation in pool
ESPNowSendPacket() {}
void release() {}
// Disable copy to prevent double-delete
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) {
this->init_data_(peer_address, payload, size);
this->callback_ = callback;
}
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
this->init_data_(peer_address, payload, size);
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
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) {
this->size_ = 0;
return;
}
this->size_ = size;
memcpy(this->data_, payload, this->size_);
}
};
} // namespace esphome::espnow
#endif // USE_ESP32

View File

@ -0,0 +1,52 @@
espnow:
auto_add_peer: false
channel: 1
peers:
- 11:22:33:44:55:66
on_receive:
- logger.log:
format: "Received 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
- espnow.send:
address: 11:22:33:44:55:66
data: "Hello from ESPHome"
on_sent:
- logger.log: "ESPNow message sent successfully"
on_error:
- logger.log: "ESPNow message failed to send"
wait_for_sent: true
continue_on_error: true
- espnow.send:
address: 11:22:33:44:55:66
data: [0x01, 0x02, 0x03, 0x04, 0x05]
- espnow.send:
address: 11:22:33:44:55:66
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
- espnow.broadcast:
data: "Hello, World!"
- espnow.broadcast:
data: [0x01, 0x02, 0x03, 0x04, 0x05]
- espnow.broadcast:
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
- espnow.peer.add:
address: 11:22:33:44:55:66
- espnow.peer.delete:
address: 11:22:33:44:55:66
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"
args:
- format_mac_address_pretty(info.src_addr).c_str()
- format_hex_pretty(data, size).c_str()
- info.rx_ctrl->rssi

View File

@ -0,0 +1 @@
<<: !include common.yaml