mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +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:
		| @@ -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 | ||||
		Reference in New Issue
	
	Block a user