mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-29 22:24:26 +00:00 
			
		
		
		
	Merge branch 'dev' into mdns_value_flash
This commit is contained in:
		| @@ -9,6 +9,7 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ACTION, | ||||
|     CONF_ACTIONS, | ||||
|     CONF_CAPTURE_RESPONSE, | ||||
|     CONF_DATA, | ||||
|     CONF_DATA_TEMPLATE, | ||||
|     CONF_EVENT, | ||||
| @@ -17,30 +18,50 @@ from esphome.const import ( | ||||
|     CONF_MAX_CONNECTIONS, | ||||
|     CONF_ON_CLIENT_CONNECTED, | ||||
|     CONF_ON_CLIENT_DISCONNECTED, | ||||
|     CONF_ON_ERROR, | ||||
|     CONF_ON_SUCCESS, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PORT, | ||||
|     CONF_REBOOT_TIMEOUT, | ||||
|     CONF_RESPONSE_TEMPLATE, | ||||
|     CONF_SERVICE, | ||||
|     CONF_SERVICES, | ||||
|     CONF_TAG, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VARIABLES, | ||||
| ) | ||||
| from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||
| from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority | ||||
| from esphome.cpp_generator import TemplateArgsType | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| DOMAIN = "api" | ||||
| DEPENDENCIES = ["network"] | ||||
| AUTO_LOAD = ["socket"] | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
|  | ||||
| def AUTO_LOAD(config: ConfigType) -> list[str]: | ||||
|     """Conditionally auto-load json only when capture_response is used.""" | ||||
|     base = ["socket"] | ||||
|  | ||||
|     # Check if any homeassistant.action/homeassistant.service has capture_response: true | ||||
|     # This flag is set during config validation in _validate_response_config | ||||
|     if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False): | ||||
|         return base + ["json"] | ||||
|  | ||||
|     return base | ||||
|  | ||||
|  | ||||
| api_ns = cg.esphome_ns.namespace("api") | ||||
| APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) | ||||
| HomeAssistantServiceCallAction = api_ns.class_( | ||||
|     "HomeAssistantServiceCallAction", automation.Action | ||||
| ) | ||||
| ActionResponse = api_ns.class_("ActionResponse") | ||||
| HomeAssistantActionResponseTrigger = api_ns.class_( | ||||
|     "HomeAssistantActionResponseTrigger", automation.Trigger | ||||
| ) | ||||
| APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) | ||||
|  | ||||
| UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) | ||||
| @@ -288,6 +309,29 @@ async def to_code(config): | ||||
| KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) | ||||
|  | ||||
|  | ||||
| def _validate_response_config(config: ConfigType) -> ConfigType: | ||||
|     # Validate dependencies: | ||||
|     # - response_template requires capture_response: true | ||||
|     # - capture_response: true requires on_success | ||||
|     if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]: | ||||
|         raise cv.Invalid( | ||||
|             f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.", | ||||
|             path=[CONF_RESPONSE_TEMPLATE], | ||||
|         ) | ||||
|  | ||||
|     if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config: | ||||
|         raise cv.Invalid( | ||||
|             f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.", | ||||
|             path=[CONF_CAPTURE_RESPONSE], | ||||
|         ) | ||||
|  | ||||
|     # Track if any action uses capture_response for AUTO_LOAD | ||||
|     if config[CONF_CAPTURE_RESPONSE]: | ||||
|         CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -303,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_VARIABLES, default={}): cv.Schema( | ||||
|                 {cv.string: cv.returning_lambda} | ||||
|             ), | ||||
|             cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string), | ||||
|             cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True), | ||||
|             cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), | ||||
|         } | ||||
|     ), | ||||
|     cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), | ||||
|     cv.rename_key(CONF_SERVICE, CONF_ACTION), | ||||
|     _validate_response_config, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -320,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( | ||||
|     HomeAssistantServiceCallAction, | ||||
|     HOMEASSISTANT_ACTION_ACTION_SCHEMA, | ||||
| ) | ||||
| async def homeassistant_service_to_code(config, action_id, template_arg, args): | ||||
| async def homeassistant_service_to_code( | ||||
|     config: ConfigType, | ||||
|     action_id: ID, | ||||
|     template_arg: cg.TemplateArguments, | ||||
|     args: TemplateArgsType, | ||||
| ): | ||||
|     cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|     serv = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, False) | ||||
| @@ -335,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args): | ||||
|     for key, value in config[CONF_VARIABLES].items(): | ||||
|         templ = await cg.templatable(value, args, None) | ||||
|         cg.add(var.add_variable(key, templ)) | ||||
|  | ||||
|     if on_error := config.get(CONF_ON_ERROR): | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS") | ||||
|         cg.add(var.set_wants_status()) | ||||
|         await automation.build_automation( | ||||
|             var.get_error_trigger(), | ||||
|             [(cg.std_string, "error"), *args], | ||||
|             on_error, | ||||
|         ) | ||||
|  | ||||
|     if on_success := config.get(CONF_ON_SUCCESS): | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") | ||||
|         cg.add(var.set_wants_status()) | ||||
|         if config[CONF_CAPTURE_RESPONSE]: | ||||
|             cg.add(var.set_wants_response()) | ||||
|             cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON") | ||||
|             await automation.build_automation( | ||||
|                 var.get_success_trigger_with_response(), | ||||
|                 [(cg.JsonObjectConst, "response"), *args], | ||||
|                 on_success, | ||||
|             ) | ||||
|  | ||||
|             if response_template := config.get(CONF_RESPONSE_TEMPLATE): | ||||
|                 templ = await cg.templatable(response_template, args, cg.std_string) | ||||
|                 cg.add(var.set_response_template(templ)) | ||||
|  | ||||
|         else: | ||||
|             await automation.build_automation( | ||||
|                 var.get_success_trigger(), | ||||
|                 args, | ||||
|                 on_success, | ||||
|             ) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -780,6 +780,22 @@ message HomeassistantActionRequest { | ||||
|   repeated HomeassistantServiceMap data_template = 3; | ||||
|   repeated HomeassistantServiceMap variables = 4; | ||||
|   bool is_event = 5; | ||||
|   uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; | ||||
|   bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; | ||||
|   string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; | ||||
| } | ||||
|  | ||||
| // Message sent by Home Assistant to ESPHome with service call response data | ||||
| message HomeassistantActionResponse { | ||||
|   option (id) = 130; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (no_delay) = true; | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"; | ||||
|  | ||||
|   uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest | ||||
|   bool success = 2; // Whether the service call succeeded | ||||
|   string error_message = 3; // Error message if success = false | ||||
|   bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; | ||||
| } | ||||
|  | ||||
| // ==================== IMPORT HOME ASSISTANT STATES ==================== | ||||
|   | ||||
| @@ -8,9 +8,9 @@ | ||||
| #endif | ||||
| #include <cerrno> | ||||
| #include <cinttypes> | ||||
| #include <utility> | ||||
| #include <functional> | ||||
| #include <limits> | ||||
| #include <utility> | ||||
| #include "esphome/components/network/util.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/entity_base.h" | ||||
| @@ -1549,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) { | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   if (msg.response_data_len > 0) { | ||||
|     this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data, | ||||
|                                           msg.response_data_len); | ||||
|   } else | ||||
| #endif | ||||
|   { | ||||
|     this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message); | ||||
|   } | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   NoiseEncryptionSetKeyResponse resp; | ||||
|   | ||||
| @@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection { | ||||
|       return; | ||||
|     this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #endif  // USE_API_HOMEASSISTANT_SERVICES | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||
|   void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||
|   | ||||
| @@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|     buffer.encode_message(4, it, true); | ||||
|   } | ||||
|   buffer.encode_bool(5, this->is_event); | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   buffer.encode_uint32(6, this->call_id); | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   buffer.encode_bool(7, this->wants_response); | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   buffer.encode_string(8, this->response_template); | ||||
| #endif | ||||
| } | ||||
| void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { | ||||
|   size.add_length(1, this->service_ref_.size()); | ||||
| @@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { | ||||
|   size.add_repeated_message(1, this->data_template); | ||||
|   size.add_repeated_message(1, this->variables); | ||||
|   size.add_bool(1, this->is_event); | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   size.add_uint32(1, this->call_id); | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   size.add_bool(1, this->wants_response); | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   size.add_length(1, this->response_template.size()); | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 1: | ||||
|       this->call_id = value.as_uint32(); | ||||
|       break; | ||||
|     case 2: | ||||
|       this->success = value.as_bool(); | ||||
|       break; | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 3: | ||||
|       this->error_message = value.as_string(); | ||||
|       break; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|     case 4: { | ||||
|       // Use raw data directly to avoid allocation | ||||
|       this->response_data = value.data(); | ||||
|       this->response_data_len = value.size(); | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   | ||||
| @@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage { | ||||
| class HomeassistantActionRequest final : public ProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint8_t MESSAGE_TYPE = 35; | ||||
|   static constexpr uint8_t ESTIMATED_SIZE = 113; | ||||
|   static constexpr uint8_t ESTIMATED_SIZE = 128; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "homeassistant_action_request"; } | ||||
| #endif | ||||
| @@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage { | ||||
|   std::vector<HomeassistantServiceMap> data_template{}; | ||||
|   std::vector<HomeassistantServiceMap> variables{}; | ||||
|   bool is_event{false}; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   uint32_t call_id{0}; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   bool wants_response{false}; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   std::string response_template{}; | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage { | ||||
|  protected: | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| class HomeassistantActionResponse final : public ProtoDecodableMessage { | ||||
|  public: | ||||
|   static constexpr uint8_t MESSAGE_TYPE = 130; | ||||
|   static constexpr uint8_t ESTIMATED_SIZE = 34; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "homeassistant_action_response"; } | ||||
| #endif | ||||
|   uint32_t call_id{0}; | ||||
|   bool success{false}; | ||||
|   std::string error_message{}; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   const uint8_t *response_data{nullptr}; | ||||
|   uint16_t response_data_len{0}; | ||||
| #endif | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { | ||||
|  public: | ||||
|   | ||||
| @@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const { | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   dump_field(out, "is_event", this->is_event); | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   dump_field(out, "call_id", this->call_id); | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   dump_field(out, "wants_response", this->wants_response); | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   dump_field(out, "response_template", this->response_template); | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| void HomeassistantActionResponse::dump_to(std::string &out) const { | ||||
|   MessageDumpHelper helper(out, "HomeassistantActionResponse"); | ||||
|   dump_field(out, "call_id", this->call_id); | ||||
|   dump_field(out, "success", this->success); | ||||
|   dump_field(out, "error_message", this->error_message); | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   out.append("  response_data: "); | ||||
|   out.append(format_hex_pretty(this->response_data, this->response_data_len)); | ||||
|   out.append("\n"); | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   | ||||
| @@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_z_wave_proxy_request(msg); | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|     case HomeassistantActionResponse::MESSAGE_TYPE: { | ||||
|       HomeassistantActionResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_homeassistant_action_response(msg); | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
|     default: | ||||
|       break; | ||||
|   | ||||
| @@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService { | ||||
|   virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; | ||||
| #endif | ||||
|   | ||||
| @@ -9,12 +9,16 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
| #include "esphome/core/version.h" | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
| #include "homeassistant_service.h" | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LOGGER | ||||
| #include "esphome/components/logger/logger.h" | ||||
| #endif | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <utility> | ||||
|  | ||||
| namespace esphome::api { | ||||
|  | ||||
| @@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call | ||||
|     client->send_homeassistant_action(call); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) { | ||||
|   this->action_response_callbacks_.push_back({call_id, std::move(callback)}); | ||||
| } | ||||
|  | ||||
| void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) { | ||||
|   for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { | ||||
|     if (it->call_id == call_id) { | ||||
|       auto callback = std::move(it->callback); | ||||
|       this->action_response_callbacks_.erase(it); | ||||
|       ActionResponse response(success, error_message); | ||||
|       callback(response); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
| void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, | ||||
|                                        const uint8_t *response_data, size_t response_data_len) { | ||||
|   for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { | ||||
|     if (it->call_id == call_id) { | ||||
|       auto callback = std::move(it->callback); | ||||
|       this->action_response_callbacks_.erase(it); | ||||
|       ActionResponse response(success, error_message, response_data, response_data_len); | ||||
|       callback(response); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #endif  // USE_API_HOMEASSISTANT_SERVICES | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
| #include "user_services.h" | ||||
| #endif | ||||
|  | ||||
| #include <map> | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome::api { | ||||
| @@ -111,7 +112,17 @@ class APIServer : public Component, public Controller { | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   void send_homeassistant_action(const HomeassistantActionRequest &call); | ||||
|  | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   // Action response handling | ||||
|   using ActionResponseCallback = std::function<void(const class ActionResponse &)>; | ||||
|   void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback); | ||||
|   void handle_action_response(uint32_t call_id, bool success, const std::string &error_message); | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   void handle_action_response(uint32_t call_id, bool success, const std::string &error_message, | ||||
|                               const uint8_t *response_data, size_t response_data_len); | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #endif  // USE_API_HOMEASSISTANT_SERVICES | ||||
| #ifdef USE_API_SERVICES | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
| @@ -187,6 +198,13 @@ class APIServer : public Component, public Controller { | ||||
| #ifdef USE_API_SERVICES | ||||
|   std::vector<UserServiceDescriptor *> user_services_; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   struct PendingActionResponse { | ||||
|     uint32_t call_id; | ||||
|     ActionResponseCallback callback; | ||||
|   }; | ||||
|   std::vector<PendingActionResponse> action_response_callbacks_; | ||||
| #endif | ||||
|  | ||||
|   // Group smaller types together | ||||
|   uint16_t port_{6053}; | ||||
|   | ||||
| @@ -3,8 +3,13 @@ | ||||
| #include "api_server.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
| #include <functional> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
| #include "api_pb2.h" | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
| #include "esphome/components/json/json_util.h" | ||||
| #endif | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| @@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair { | ||||
|   TemplatableStringValue<Ts...> value; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| // Represents the response data from a Home Assistant action | ||||
| class ActionResponse { | ||||
|  public: | ||||
|   ActionResponse(bool success, std::string error_message = "") | ||||
|       : success_(success), error_message_(std::move(error_message)) {} | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len) | ||||
|       : success_(success), error_message_(std::move(error_message)) { | ||||
|     if (data == nullptr || data_len == 0) | ||||
|       return; | ||||
|     this->json_document_ = json::parse_json(data, data_len); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   bool is_success() const { return this->success_; } | ||||
|   const std::string &get_error_message() const { return this->error_message_; } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   // Get data as parsed JSON object (const version returns read-only view) | ||||
|   JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); } | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool success_; | ||||
|   std::string error_message_; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   JsonDocument json_document_; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| // Callback type for action responses | ||||
| template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>; | ||||
| #endif | ||||
|  | ||||
| template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> { | ||||
|  public: | ||||
|   explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} | ||||
|   explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) { | ||||
|     this->flags_.is_event = is_event; | ||||
|   } | ||||
|  | ||||
|   template<typename T> void set_service(T service) { this->service_ = service; } | ||||
|  | ||||
| @@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|     this->variables_.emplace_back(std::move(key), value); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|   template<typename T> void set_response_template(T response_template) { | ||||
|     this->response_template_ = response_template; | ||||
|     this->flags_.has_response_template = true; | ||||
|   } | ||||
|  | ||||
|   void set_wants_status() { this->flags_.wants_status = true; } | ||||
|   void set_wants_response() { this->flags_.wants_response = true; } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const { | ||||
|     return this->success_trigger_with_response_; | ||||
|   } | ||||
| #endif | ||||
|   Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; } | ||||
|   Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; } | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|  | ||||
|   void play(Ts... x) override { | ||||
|     HomeassistantActionRequest resp; | ||||
|     std::string service_value = this->service_.value(x...); | ||||
|     resp.set_service(StringRef(service_value)); | ||||
|     resp.is_event = this->is_event_; | ||||
|     resp.is_event = this->flags_.is_event; | ||||
|     for (auto &it : this->data_) { | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
| @@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|     } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|     if (this->flags_.wants_status) { | ||||
|       // Generate a unique call ID for this service call | ||||
|       static uint32_t call_id_counter = 1; | ||||
|       uint32_t call_id = call_id_counter++; | ||||
|       resp.call_id = call_id; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|       if (this->flags_.wants_response) { | ||||
|         resp.wants_response = true; | ||||
|         // Set response template if provided | ||||
|         if (this->flags_.has_response_template) { | ||||
|           std::string response_template_value = this->response_template_.value(x...); | ||||
|           resp.response_template = response_template_value; | ||||
|         } | ||||
|       } | ||||
| #endif | ||||
|  | ||||
|       auto captured_args = std::make_tuple(x...); | ||||
|       this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) { | ||||
|         std::apply( | ||||
|             [this, &response](auto &&...args) { | ||||
|               if (response.is_success()) { | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|                 if (this->flags_.wants_response) { | ||||
|                   this->success_trigger_with_response_->trigger(response.get_json(), args...); | ||||
|                 } else | ||||
| #endif | ||||
|                 { | ||||
|                   this->success_trigger_->trigger(args...); | ||||
|                 } | ||||
|               } else { | ||||
|                 this->error_trigger_->trigger(response.get_error_message(), args...); | ||||
|               } | ||||
|             }, | ||||
|             captured_args); | ||||
|       }); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     this->parent_->send_homeassistant_action(resp); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   APIServer *parent_; | ||||
|   bool is_event_; | ||||
|   TemplatableStringValue<Ts...> service_{}; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> data_; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> data_template_; | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> variables_; | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   TemplatableStringValue<Ts...> response_template_{""}; | ||||
|   Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>(); | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON | ||||
|   Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>(); | ||||
|   Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>(); | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
|  | ||||
|   struct Flags { | ||||
|     uint8_t is_event : 1; | ||||
|     uint8_t wants_status : 1; | ||||
|     uint8_t wants_response : 1; | ||||
|     uint8_t has_response_template : 1; | ||||
|     uint8_t reserved : 5; | ||||
|   } flags_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome::api | ||||
|  | ||||
| #endif | ||||
| #endif | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/epaper_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/epaper_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
							
								
								
									
										80
									
								
								esphome/components/epaper_spi/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/epaper_spi/display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| from esphome import core, pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import display, spi | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BUSY_PIN, | ||||
|     CONF_DC_PIN, | ||||
|     CONF_ID, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MODEL, | ||||
|     CONF_PAGES, | ||||
|     CONF_RESET_DURATION, | ||||
|     CONF_RESET_PIN, | ||||
| ) | ||||
|  | ||||
| AUTO_LOAD = ["split_buffer"] | ||||
| DEPENDENCIES = ["spi"] | ||||
|  | ||||
| epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") | ||||
| EPaperBase = epaper_spi_ns.class_( | ||||
|     "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer | ||||
| ) | ||||
|  | ||||
| EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) | ||||
| EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) | ||||
|  | ||||
| MODELS = { | ||||
|     "7.3in-spectra-e6": EPaper7p3InSpectraE6, | ||||
| } | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     display.FULL_DISPLAY_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(EPaperBase), | ||||
|             cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), | ||||
|             cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, | ||||
|             cv.Optional(CONF_RESET_DURATION): cv.All( | ||||
|                 cv.positive_time_period_milliseconds, | ||||
|                 cv.Range(max=core.TimePeriod(milliseconds=500)), | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(spi.spi_device_schema()), | ||||
|     cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( | ||||
|     "epaper_spi", require_miso=False, require_mosi=True | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|  | ||||
|     rhs = model.new() | ||||
|     var = cg.Pvariable(config[CONF_ID], rhs, model) | ||||
|  | ||||
|     await display.register_display(var, config) | ||||
|     await spi.register_spi_device(var, config) | ||||
|  | ||||
|     dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) | ||||
|     cg.add(var.set_dc_pin(dc)) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         lambda_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void | ||||
|         ) | ||||
|         cg.add(var.set_writer(lambda_)) | ||||
|     if CONF_RESET_PIN in config: | ||||
|         reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) | ||||
|         cg.add(var.set_reset_pin(reset)) | ||||
|     if CONF_BUSY_PIN in config: | ||||
|         busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) | ||||
|         cg.add(var.set_busy_pin(busy)) | ||||
|     if CONF_RESET_DURATION in config: | ||||
|         cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) | ||||
							
								
								
									
										227
									
								
								esphome/components/epaper_spi/epaper_spi.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								esphome/components/epaper_spi/epaper_spi.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,227 @@ | ||||
| #include "epaper_spi.h" | ||||
| #include <cinttypes> | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome::epaper_spi { | ||||
|  | ||||
| static const char *const TAG = "epaper_spi"; | ||||
|  | ||||
| static const LogString *epaper_state_to_string(EPaperState state) { | ||||
|   switch (state) { | ||||
|     case EPaperState::IDLE: | ||||
|       return LOG_STR("IDLE"); | ||||
|     case EPaperState::UPDATE: | ||||
|       return LOG_STR("UPDATE"); | ||||
|     case EPaperState::RESET: | ||||
|       return LOG_STR("RESET"); | ||||
|     case EPaperState::INITIALISE: | ||||
|       return LOG_STR("INITIALISE"); | ||||
|     case EPaperState::TRANSFER_DATA: | ||||
|       return LOG_STR("TRANSFER_DATA"); | ||||
|     case EPaperState::POWER_ON: | ||||
|       return LOG_STR("POWER_ON"); | ||||
|     case EPaperState::REFRESH_SCREEN: | ||||
|       return LOG_STR("REFRESH_SCREEN"); | ||||
|     case EPaperState::POWER_OFF: | ||||
|       return LOG_STR("POWER_OFF"); | ||||
|     case EPaperState::DEEP_SLEEP: | ||||
|       return LOG_STR("DEEP_SLEEP"); | ||||
|     default: | ||||
|       return LOG_STR("UNKNOWN"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void EPaperBase::setup() { | ||||
|   if (!this->init_buffer_(this->get_buffer_length())) { | ||||
|     this->mark_failed("Failed to initialise buffer"); | ||||
|     return; | ||||
|   } | ||||
|   this->setup_pins_(); | ||||
|   this->spi_setup(); | ||||
| } | ||||
|  | ||||
| bool EPaperBase::init_buffer_(size_t buffer_length) { | ||||
|   if (!this->buffer_.init(buffer_length)) { | ||||
|     return false; | ||||
|   } | ||||
|   this->clear(); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void EPaperBase::setup_pins_() { | ||||
|   this->dc_pin_->setup();  // OUTPUT | ||||
|   this->dc_pin_->digital_write(false); | ||||
|  | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup();  // OUTPUT | ||||
|     this->reset_pin_->digital_write(true); | ||||
|   } | ||||
|  | ||||
|   if (this->busy_pin_ != nullptr) { | ||||
|     this->busy_pin_->setup();  // INPUT | ||||
|   } | ||||
| } | ||||
|  | ||||
| float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } | ||||
|  | ||||
| void EPaperBase::command(uint8_t value) { | ||||
|   this->start_command_(); | ||||
|   this->write_byte(value); | ||||
|   this->end_command_(); | ||||
| } | ||||
|  | ||||
| void EPaperBase::data(uint8_t value) { | ||||
|   this->start_data_(); | ||||
|   this->write_byte(value); | ||||
|   this->end_data_(); | ||||
| } | ||||
|  | ||||
| // write a command followed by zero or more bytes of data. | ||||
| // The command is the first byte, length is the length of data only in the second byte, followed by the data. | ||||
| // [COMMAND, LENGTH, DATA...] | ||||
| void EPaperBase::cmd_data(const uint8_t *data) { | ||||
|   const uint8_t command = data[0]; | ||||
|   const uint8_t length = data[1]; | ||||
|   const uint8_t *ptr = data + 2; | ||||
|  | ||||
|   ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, | ||||
|             format_hex_pretty(ptr, length, '.', false).c_str()); | ||||
|  | ||||
|   this->dc_pin_->digital_write(false); | ||||
|   this->enable(); | ||||
|   this->write_byte(command); | ||||
|   if (length > 0) { | ||||
|     this->dc_pin_->digital_write(true); | ||||
|     this->write_array(ptr, length); | ||||
|   } | ||||
|   this->disable(); | ||||
| } | ||||
|  | ||||
| bool EPaperBase::is_idle_() { | ||||
|   if (this->busy_pin_ == nullptr) { | ||||
|     return true; | ||||
|   } | ||||
|   return !this->busy_pin_->digital_read(); | ||||
| } | ||||
|  | ||||
| void EPaperBase::reset() { | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->digital_write(false); | ||||
|     this->disable_loop(); | ||||
|     this->set_timeout(this->reset_duration_, [this] { | ||||
|       this->reset_pin_->digital_write(true); | ||||
|       this->set_timeout(20, [this] { this->enable_loop(); }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void EPaperBase::update() { | ||||
|   if (!this->state_queue_.empty()) { | ||||
|     ESP_LOGE(TAG, "Display update already in progress - %s", | ||||
|              LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front()))); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->state_queue_.push(EPaperState::UPDATE); | ||||
|   this->state_queue_.push(EPaperState::RESET); | ||||
|   this->state_queue_.push(EPaperState::INITIALISE); | ||||
|   this->state_queue_.push(EPaperState::TRANSFER_DATA); | ||||
|   this->state_queue_.push(EPaperState::POWER_ON); | ||||
|   this->state_queue_.push(EPaperState::REFRESH_SCREEN); | ||||
|   this->state_queue_.push(EPaperState::POWER_OFF); | ||||
|   this->state_queue_.push(EPaperState::DEEP_SLEEP); | ||||
|   this->state_queue_.push(EPaperState::IDLE); | ||||
|  | ||||
|   this->enable_loop(); | ||||
| } | ||||
|  | ||||
| void EPaperBase::loop() { | ||||
|   if (this->waiting_for_idle_) { | ||||
|     if (this->is_idle_()) { | ||||
|       this->waiting_for_idle_ = false; | ||||
|     } else { | ||||
|       if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) { | ||||
|         ESP_LOGV(TAG, "Waiting for idle"); | ||||
|         this->waiting_for_idle_last_print_ = App.get_loop_component_start_time(); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   auto state = this->state_queue_.front(); | ||||
|  | ||||
|   switch (state) { | ||||
|     case EPaperState::IDLE: | ||||
|       this->disable_loop(); | ||||
|       break; | ||||
|     case EPaperState::UPDATE: | ||||
|       this->do_update_();  // Calls ESPHome (current page) lambda | ||||
|       break; | ||||
|     case EPaperState::RESET: | ||||
|       this->reset(); | ||||
|       break; | ||||
|     case EPaperState::INITIALISE: | ||||
|       this->initialise_(); | ||||
|       break; | ||||
|     case EPaperState::TRANSFER_DATA: | ||||
|       if (!this->transfer_data()) { | ||||
|         return;  // Not done yet, come back next loop | ||||
|       } | ||||
|       break; | ||||
|     case EPaperState::POWER_ON: | ||||
|       this->power_on(); | ||||
|       break; | ||||
|     case EPaperState::REFRESH_SCREEN: | ||||
|       this->refresh_screen(); | ||||
|       break; | ||||
|     case EPaperState::POWER_OFF: | ||||
|       this->power_off(); | ||||
|       break; | ||||
|     case EPaperState::DEEP_SLEEP: | ||||
|       this->deep_sleep(); | ||||
|       break; | ||||
|   } | ||||
|   this->state_queue_.pop(); | ||||
| } | ||||
|  | ||||
| void EPaperBase::start_command_() { | ||||
|   this->dc_pin_->digital_write(false); | ||||
|   this->enable(); | ||||
| } | ||||
|  | ||||
| void EPaperBase::end_command_() { this->disable(); } | ||||
|  | ||||
| void EPaperBase::start_data_() { | ||||
|   this->dc_pin_->digital_write(true); | ||||
|   this->enable(); | ||||
| } | ||||
| void EPaperBase::end_data_() { this->disable(); } | ||||
|  | ||||
| void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } | ||||
|  | ||||
| void EPaperBase::initialise_() { | ||||
|   size_t index = 0; | ||||
|   const auto &sequence = this->init_sequence_; | ||||
|   const size_t sequence_size = this->init_sequence_length_; | ||||
|   while (index != sequence_size) { | ||||
|     if (sequence_size - index < 2) { | ||||
|       this->mark_failed("Malformed init sequence"); | ||||
|       return; | ||||
|     } | ||||
|     const auto *ptr = sequence + index; | ||||
|     const uint8_t length = ptr[1]; | ||||
|     if (sequence_size - index < length + 2) { | ||||
|       this->mark_failed("Malformed init sequence"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this->cmd_data(ptr); | ||||
|     index += length + 2; | ||||
|   } | ||||
|  | ||||
|   this->power_on(); | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::epaper_spi | ||||
							
								
								
									
										93
									
								
								esphome/components/epaper_spi/epaper_spi.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								esphome/components/epaper_spi/epaper_spi.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/display/display_buffer.h" | ||||
| #include "esphome/components/spi/spi.h" | ||||
| #include "esphome/components/split_buffer/split_buffer.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include <queue> | ||||
|  | ||||
| namespace esphome::epaper_spi { | ||||
|  | ||||
| enum class EPaperState : uint8_t { | ||||
|   IDLE, | ||||
|   UPDATE, | ||||
|   RESET, | ||||
|   INITIALISE, | ||||
|   TRANSFER_DATA, | ||||
|   POWER_ON, | ||||
|   REFRESH_SCREEN, | ||||
|   POWER_OFF, | ||||
|   DEEP_SLEEP, | ||||
| }; | ||||
|  | ||||
| static const uint8_t MAX_TRANSFER_TIME = 10;  // Transfer in 10ms blocks to allow the loop to run | ||||
|  | ||||
| class EPaperBase : public display::DisplayBuffer, | ||||
|                    public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||
|                                          spi::DATA_RATE_2MHZ> { | ||||
|  public: | ||||
|   EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) | ||||
|       : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} | ||||
|   void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } | ||||
|   float get_setup_priority() const override; | ||||
|   void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } | ||||
|   void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } | ||||
|   void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } | ||||
|  | ||||
|   void command(uint8_t value); | ||||
|   void data(uint8_t value); | ||||
|   void cmd_data(const uint8_t *data); | ||||
|  | ||||
|   void update() override; | ||||
|   void loop() override; | ||||
|  | ||||
|   void setup() override; | ||||
|  | ||||
|   void on_safe_shutdown() override; | ||||
|  | ||||
|  protected: | ||||
|   bool is_idle_(); | ||||
|   void setup_pins_(); | ||||
|   virtual void reset(); | ||||
|   void initialise_(); | ||||
|   bool init_buffer_(size_t buffer_length); | ||||
|  | ||||
|   virtual int get_width_controller() { return this->get_width_internal(); }; | ||||
|   virtual void deep_sleep() = 0; | ||||
|   /** | ||||
|    * Send data to the device via SPI | ||||
|    * @return true if done, false if should be called next loop | ||||
|    */ | ||||
|   virtual bool transfer_data() = 0; | ||||
|   virtual void refresh_screen() = 0; | ||||
|  | ||||
|   virtual void power_on() = 0; | ||||
|   virtual void power_off() = 0; | ||||
|   virtual uint32_t get_buffer_length() = 0; | ||||
|  | ||||
|   void start_command_(); | ||||
|   void end_command_(); | ||||
|   void start_data_(); | ||||
|   void end_data_(); | ||||
|  | ||||
|   const size_t init_sequence_length_{0}; | ||||
|  | ||||
|   size_t current_data_index_{0}; | ||||
|   uint32_t reset_duration_{200}; | ||||
|   uint32_t waiting_for_idle_last_print_{0}; | ||||
|  | ||||
|   GPIOPin *dc_pin_; | ||||
|   GPIOPin *busy_pin_{nullptr}; | ||||
|   GPIOPin *reset_pin_{nullptr}; | ||||
|  | ||||
|   const uint8_t *init_sequence_{nullptr}; | ||||
|  | ||||
|   bool waiting_for_idle_{false}; | ||||
|  | ||||
|   split_buffer::SplitBuffer buffer_; | ||||
|  | ||||
|   std::queue<EPaperState> state_queue_{{EPaperState::IDLE}}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome::epaper_spi | ||||
| @@ -0,0 +1,42 @@ | ||||
| #include "epaper_spi_model_7p3in_spectra_e6.h" | ||||
|  | ||||
| namespace esphome::epaper_spi { | ||||
|  | ||||
| static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6"; | ||||
|  | ||||
| void EPaper7p3InSpectraE6::power_on() { | ||||
|   ESP_LOGI(TAG, "Power on"); | ||||
|   this->command(0x04); | ||||
|   this->waiting_for_idle_ = true; | ||||
| } | ||||
|  | ||||
| void EPaper7p3InSpectraE6::power_off() { | ||||
|   ESP_LOGI(TAG, "Power off"); | ||||
|   this->command(0x02); | ||||
|   this->data(0x00); | ||||
|   this->waiting_for_idle_ = true; | ||||
| } | ||||
|  | ||||
| void EPaper7p3InSpectraE6::refresh_screen() { | ||||
|   ESP_LOGI(TAG, "Refresh"); | ||||
|   this->command(0x12); | ||||
|   this->data(0x00); | ||||
|   this->waiting_for_idle_ = true; | ||||
| } | ||||
|  | ||||
| void EPaper7p3InSpectraE6::deep_sleep() { | ||||
|   ESP_LOGI(TAG, "Deep sleep"); | ||||
|   this->command(0x07); | ||||
|   this->data(0xA5); | ||||
| } | ||||
|  | ||||
| void EPaper7p3InSpectraE6::dump_config() { | ||||
|   LOG_DISPLAY("", "E-Paper SPI", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Model: 7.3in Spectra E6"); | ||||
|   LOG_PIN("  Reset Pin: ", this->reset_pin_); | ||||
|   LOG_PIN("  DC Pin: ", this->dc_pin_); | ||||
|   LOG_PIN("  Busy Pin: ", this->busy_pin_); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::epaper_spi | ||||
| @@ -0,0 +1,45 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "epaper_spi_spectra_e6.h" | ||||
|  | ||||
| namespace esphome::epaper_spi { | ||||
|  | ||||
| class EPaper7p3InSpectraE6 : public EPaperSpectraE6 { | ||||
|   static constexpr const uint16_t WIDTH = 800; | ||||
|   static constexpr const uint16_t HEIGHT = 480; | ||||
|   // clang-format off | ||||
|  | ||||
|   // Command, data length, data | ||||
|   static constexpr uint8_t INIT_SEQUENCE[] = { | ||||
|     0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18, | ||||
|     0x01, 1, 0x3F, | ||||
|     0x00, 2, 0x5F, 0x69, | ||||
|     0x03, 4, 0x00, 0x54, 0x00, 0x44, | ||||
|     0x05, 4, 0x40, 0x1F, 0x1F, 0x2C, | ||||
|     0x06, 4, 0x6F, 0x1F, 0x17, 0x49, | ||||
|     0x08, 4, 0x6F, 0x1F, 0x1F, 0x22, | ||||
|     0x30, 1, 0x03, | ||||
|     0x50, 1, 0x3F, | ||||
|     0x60, 2, 0x02, 0x00, | ||||
|     0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256, | ||||
|     0x84, 1, 0x01, | ||||
|     0xE3, 1, 0x2F, | ||||
|   }; | ||||
|   // clang-format on | ||||
|  | ||||
|  public: | ||||
|   EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {} | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   int get_width_internal() override { return WIDTH; }; | ||||
|   int get_height_internal() override { return HEIGHT; }; | ||||
|  | ||||
|   void refresh_screen() override; | ||||
|   void power_on() override; | ||||
|   void power_off() override; | ||||
|   void deep_sleep() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome::epaper_spi | ||||
							
								
								
									
										135
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| #include "epaper_spi_spectra_e6.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome::epaper_spi { | ||||
|  | ||||
| static constexpr const char *const TAG = "epaper_spi.6c"; | ||||
|  | ||||
| static inline uint8_t color_to_hex(Color color) { | ||||
|   if (color.red > 127) { | ||||
|     if (color.green > 170) { | ||||
|       if (color.blue > 127) { | ||||
|         return 0x1;  // White | ||||
|       } else { | ||||
|         return 0x2;  // Yellow | ||||
|       } | ||||
|     } else { | ||||
|       return 0x3;  // Red (or Magenta) | ||||
|     } | ||||
|   } else { | ||||
|     if (color.green > 127) { | ||||
|       if (color.blue > 127) { | ||||
|         return 0x5;  // Cyan -> Blue | ||||
|       } else { | ||||
|         return 0x6;  // Green | ||||
|       } | ||||
|     } else { | ||||
|       if (color.blue > 127) { | ||||
|         return 0x5;  // Blue | ||||
|       } else { | ||||
|         return 0x0;  // Black | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void EPaperSpectraE6::fill(Color color) { | ||||
|   uint8_t pixel_color; | ||||
|   if (color.is_on()) { | ||||
|     pixel_color = color_to_hex(color); | ||||
|   } else { | ||||
|     pixel_color = 0x1; | ||||
|   } | ||||
|  | ||||
|   // We store 8 bitset<3> in 3 bytes | ||||
|   // | byte 1 | byte 2 | byte 3 | | ||||
|   // |aaabbbaa|abbbaaab|bbaaabbb| | ||||
|   uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1; | ||||
|   uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2; | ||||
|   uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0; | ||||
|  | ||||
|   const size_t buffer_length = this->get_buffer_length(); | ||||
|   for (size_t i = 0; i < buffer_length; i += 3) { | ||||
|     this->buffer_[i + 0] = byte_1; | ||||
|     this->buffer_[i + 1] = byte_2; | ||||
|     this->buffer_[i + 2] = byte_3; | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint32_t EPaperSpectraE6::get_buffer_length() { | ||||
|   // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes | ||||
|   return this->get_width_controller() * this->get_height_internal() / 8u * 3u; | ||||
| } | ||||
|  | ||||
| void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
|   if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) | ||||
|     return; | ||||
|  | ||||
|   uint8_t pixel_bits = color_to_hex(color); | ||||
|   uint32_t pixel_position = x + y * this->get_width_controller(); | ||||
|   uint32_t first_bit_position = pixel_position * 3; | ||||
|   uint32_t byte_position = first_bit_position / 8u; | ||||
|   uint32_t byte_subposition = first_bit_position % 8u; | ||||
|  | ||||
|   if (byte_subposition <= 5) { | ||||
|     this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) | | ||||
|                                    (pixel_bits << (5 - byte_subposition)); | ||||
|   } else { | ||||
|     this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | | ||||
|                                    (pixel_bits >> (byte_subposition - 5)); | ||||
|  | ||||
|     this->buffer_[byte_position + 1] = | ||||
|         (this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) | | ||||
|         (pixel_bits << (13 - byte_subposition)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool HOT EPaperSpectraE6::transfer_data() { | ||||
|   const uint32_t start_time = App.get_loop_component_start_time(); | ||||
|   if (this->current_data_index_ == 0) { | ||||
|     ESP_LOGV(TAG, "Sending data"); | ||||
|     this->command(0x10); | ||||
|   } | ||||
|  | ||||
|   uint8_t bytes_to_send[4]{0}; | ||||
|   const size_t buffer_length = this->get_buffer_length(); | ||||
|   for (size_t i = this->current_data_index_; i < buffer_length; i += 3) { | ||||
|     const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); | ||||
|     // 8 pixels are stored in 3 bytes | ||||
|     // |aaabbbaa|abbbaaab|bbaaabbb| | ||||
|     // | byte 1 | byte 2 | byte 3 | | ||||
|     bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111); | ||||
|     bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111); | ||||
|     bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111); | ||||
|     bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111); | ||||
|  | ||||
|     this->start_data_(); | ||||
|     this->write_array(bytes_to_send, sizeof(bytes_to_send)); | ||||
|     this->end_data_(); | ||||
|  | ||||
|     if (millis() - start_time > MAX_TRANSFER_TIME) { | ||||
|       // Let the main loop run and come back next loop | ||||
|       this->current_data_index_ = i + 3; | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|   // Finished the entire dataset | ||||
|   this->current_data_index_ = 0; | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void EPaperSpectraE6::reset() { | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->disable_loop(); | ||||
|     this->reset_pin_->digital_write(true); | ||||
|     this->set_timeout(20, [this] { | ||||
|       this->reset_pin_->digital_write(false); | ||||
|       delay(2); | ||||
|       this->reset_pin_->digital_write(true); | ||||
|       this->set_timeout(20, [this] { this->enable_loop(); }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::epaper_spi | ||||
							
								
								
									
										23
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "epaper_spi.h" | ||||
|  | ||||
| namespace esphome::epaper_spi { | ||||
|  | ||||
| class EPaperSpectraE6 : public EPaperBase { | ||||
|  public: | ||||
|   EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length) | ||||
|       : EPaperBase(init_sequence, init_sequence_length) {} | ||||
|  | ||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||
|   void fill(Color color) override; | ||||
|  | ||||
|  protected: | ||||
|   void draw_absolute_pixel_internal(int x, int y, Color color) override; | ||||
|   uint32_t get_buffer_length() override; | ||||
|  | ||||
|   bool transfer_data() override; | ||||
|   void reset() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome::epaper_spi | ||||
							
								
								
									
										5
									
								
								esphome/components/split_buffer/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/split_buffer/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
|  | ||||
| # Allows split_buffer to be configured in yaml, to allow use of the C++ api. | ||||
|  | ||||
| CONFIG_SCHEMA = {} | ||||
							
								
								
									
										133
									
								
								esphome/components/split_buffer/split_buffer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								esphome/components/split_buffer/split_buffer.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| #include "split_buffer.h" | ||||
|  | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome::split_buffer { | ||||
|  | ||||
| static constexpr const char *const TAG = "split_buffer"; | ||||
|  | ||||
| SplitBuffer::~SplitBuffer() { this->free(); } | ||||
|  | ||||
| bool SplitBuffer::init(size_t total_length) { | ||||
|   this->free();  // Clean up any existing allocation | ||||
|  | ||||
|   if (total_length == 0) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   this->total_length_ = total_length; | ||||
|   size_t current_buffer_size = total_length; | ||||
|  | ||||
|   RAMAllocator<uint8_t *> ptr_allocator; | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|  | ||||
|   // Try to allocate the entire buffer first | ||||
|   while (current_buffer_size > 0) { | ||||
|     // Calculate how many buffers we need of this size | ||||
|     size_t needed_buffers = (total_length + current_buffer_size - 1) / current_buffer_size; | ||||
|  | ||||
|     // Try to allocate array of buffer pointers | ||||
|     uint8_t **temp_buffers = ptr_allocator.allocate(needed_buffers); | ||||
|     if (temp_buffers == nullptr) { | ||||
|       // If we can't even allocate the pointer array, don't need to continue | ||||
|       ESP_LOGE(TAG, "Failed to allocate pointers"); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     // Initialize all pointers to null | ||||
|     for (size_t i = 0; i < needed_buffers; i++) { | ||||
|       temp_buffers[i] = nullptr; | ||||
|     } | ||||
|  | ||||
|     // Try to allocate all the buffers | ||||
|     bool allocation_success = true; | ||||
|     for (size_t i = 0; i < needed_buffers; i++) { | ||||
|       size_t this_buffer_size = current_buffer_size; | ||||
|       // Last buffer might be smaller if total_length is not divisible by current_buffer_size | ||||
|       if (i == needed_buffers - 1 && total_length % current_buffer_size != 0) { | ||||
|         this_buffer_size = total_length % current_buffer_size; | ||||
|       } | ||||
|  | ||||
|       temp_buffers[i] = allocator.allocate(this_buffer_size); | ||||
|       if (temp_buffers[i] == nullptr) { | ||||
|         allocation_success = false; | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       // Initialize buffer to zero | ||||
|       memset(temp_buffers[i], 0, this_buffer_size); | ||||
|     } | ||||
|  | ||||
|     if (allocation_success) { | ||||
|       // Success! Store the result | ||||
|       this->buffers_ = temp_buffers; | ||||
|       this->buffer_count_ = needed_buffers; | ||||
|       this->buffer_size_ = current_buffer_size; | ||||
|       ESP_LOGD(TAG, "Allocated %zu * %zu bytes - %zu bytes", this->buffer_count_, this->buffer_size_, | ||||
|                this->total_length_); | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     // Allocation failed, clean up and try smaller buffers | ||||
|     for (size_t i = 0; i < needed_buffers; i++) { | ||||
|       if (temp_buffers[i] != nullptr) { | ||||
|         allocator.deallocate(temp_buffers[i], 0); | ||||
|       } | ||||
|     } | ||||
|     ptr_allocator.deallocate(temp_buffers, 0); | ||||
|  | ||||
|     // Halve the buffer size and try again | ||||
|     current_buffer_size = current_buffer_size / 2; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGE(TAG, "Failed to allocate %zu bytes", total_length); | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void SplitBuffer::free() { | ||||
|   if (this->buffers_ != nullptr) { | ||||
|     RAMAllocator<uint8_t> allocator; | ||||
|     for (size_t i = 0; i < this->buffer_count_; i++) { | ||||
|       if (this->buffers_[i] != nullptr) { | ||||
|         allocator.deallocate(this->buffers_[i], 0); | ||||
|       } | ||||
|     } | ||||
|     RAMAllocator<uint8_t *> ptr_allocator; | ||||
|     ptr_allocator.deallocate(this->buffers_, 0); | ||||
|     this->buffers_ = nullptr; | ||||
|   } | ||||
|   this->buffer_count_ = 0; | ||||
|   this->buffer_size_ = 0; | ||||
|   this->total_length_ = 0; | ||||
| } | ||||
|  | ||||
| uint8_t &SplitBuffer::operator[](size_t index) { | ||||
|   if (index >= this->total_length_) { | ||||
|     ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); | ||||
|     // Return reference to a static dummy byte to avoid crash | ||||
|     static uint8_t dummy = 0; | ||||
|     return dummy; | ||||
|   } | ||||
|  | ||||
|   size_t buffer_index = index / this->buffer_size_; | ||||
|   size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; | ||||
|  | ||||
|   return this->buffers_[buffer_index][offset_in_buffer]; | ||||
| } | ||||
|  | ||||
| const uint8_t &SplitBuffer::operator[](size_t index) const { | ||||
|   if (index >= this->total_length_) { | ||||
|     ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); | ||||
|     // Return reference to a static dummy byte to avoid crash | ||||
|     static const uint8_t DUMMY = 0; | ||||
|     return DUMMY; | ||||
|   } | ||||
|  | ||||
|   size_t buffer_index = index / this->buffer_size_; | ||||
|   size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; | ||||
|  | ||||
|   return this->buffers_[buffer_index][offset_in_buffer]; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::split_buffer | ||||
							
								
								
									
										40
									
								
								esphome/components/split_buffer/split_buffer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/split_buffer/split_buffer.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
| #include <cstdlib> | ||||
|  | ||||
| namespace esphome::split_buffer { | ||||
|  | ||||
| class SplitBuffer { | ||||
|  public: | ||||
|   SplitBuffer() = default; | ||||
|   ~SplitBuffer(); | ||||
|  | ||||
|   // Initialize the buffer with the desired total length | ||||
|   bool init(size_t total_length); | ||||
|  | ||||
|   // Free all allocated buffers | ||||
|   void free(); | ||||
|  | ||||
|   // Access operators | ||||
|   uint8_t &operator[](size_t index); | ||||
|   const uint8_t &operator[](size_t index) const; | ||||
|  | ||||
|   // Get the total length | ||||
|   size_t size() const { return this->total_length_; } | ||||
|  | ||||
|   // Get buffer information | ||||
|   size_t get_buffer_count() const { return this->buffer_count_; } | ||||
|   size_t get_buffer_size() const { return this->buffer_size_; } | ||||
|  | ||||
|   // Check if successfully initialized | ||||
|   bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; } | ||||
|  | ||||
|  private: | ||||
|   uint8_t **buffers_{nullptr}; | ||||
|   size_t buffer_count_{0}; | ||||
|   size_t buffer_size_{0}; | ||||
|   size_t total_length_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome::split_buffer | ||||
		Reference in New Issue
	
	Block a user