mirror of
https://github.com/esphome/esphome.git
synced 2025-08-02 00:17:48 +00:00
[espnow] Basic communication between ESP32 devices (#9582)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
291215909a
commit
c42c5dd946
@ -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
|
||||
|
320
esphome/components/espnow/__init__.py
Normal file
320
esphome/components/espnow/__init__.py
Normal 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
|
175
esphome/components/espnow/automation.h
Normal file
175
esphome/components/espnow/automation.h
Normal 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
|
468
esphome/components/espnow/espnow_component.cpp
Normal file
468
esphome/components/espnow/espnow_component.cpp
Normal 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
|
182
esphome/components/espnow/espnow_component.h
Normal file
182
esphome/components/espnow/espnow_component.h
Normal 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
|
19
esphome/components/espnow/espnow_err.h
Normal file
19
esphome/components/espnow/espnow_err.h
Normal 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
|
166
esphome/components/espnow/espnow_packet.h
Normal file
166
esphome/components/espnow/espnow_packet.h
Normal 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
|
52
tests/components/espnow/common.yaml
Normal file
52
tests/components/espnow/common.yaml
Normal 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
|
1
tests/components/espnow/test.esp32-idf.yaml
Normal file
1
tests/components/espnow/test.esp32-idf.yaml
Normal file
@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
Loading…
x
Reference in New Issue
Block a user