mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +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/esp32_rmt_led_strip/* @jesserockz | ||||||
| esphome/components/esp8266/* @esphome/core | esphome/components/esp8266/* @esphome/core | ||||||
| esphome/components/esp_ldo/* @clydebarrow | esphome/components/esp_ldo/* @clydebarrow | ||||||
|  | esphome/components/espnow/* @jesserockz | ||||||
| esphome/components/ethernet_info/* @gtjadsonsantos | esphome/components/ethernet_info/* @gtjadsonsantos | ||||||
| esphome/components/event/* @nohat | esphome/components/event/* @nohat | ||||||
| esphome/components/event_emitter/* @Rapsssito | 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