From 8aa8bb8f9898760dfb169c57706b91e9833da785 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 07:45:32 +1000 Subject: [PATCH 1/2] [epaper_spi] Refactoring (#11540) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/display/display.h | 2 +- esphome/components/epaper_spi/display.py | 124 ++++++++-- esphome/components/epaper_spi/epaper_spi.cpp | 210 ++++++++++------- esphome/components/epaper_spi/epaper_spi.h | 104 ++++++--- .../epaper_spi_model_7p3in_spectra_e6.cpp | 42 ---- .../epaper_spi_model_7p3in_spectra_e6.h | 45 ---- .../epaper_spi/epaper_spi_spectra_e6.cpp | 221 ++++++++++-------- .../epaper_spi/epaper_spi_spectra_e6.h | 15 +- .../components/epaper_spi/models/__init__.py | 65 ++++++ .../epaper_spi/models/spectra_e6.py | 51 ++++ esphome/components/mipi/__init__.py | 23 +- .../components/split_buffer/split_buffer.cpp | 43 ++-- .../components/split_buffer/split_buffer.h | 10 +- .../epaper_spi/test.esp32-s3-idf.yaml | 8 +- 14 files changed, 619 insertions(+), 344 deletions(-) delete mode 100644 esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp delete mode 100644 esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h create mode 100644 esphome/components/epaper_spi/models/__init__.py create mode 100644 esphome/components/epaper_spi/models/spectra_e6.py diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index f2d79d12d9..14205da853 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -210,7 +210,7 @@ class Display : public PollingComponent { /// Fill the entire screen with the given color. virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. - void clear(); + virtual void clear(); /// Get the calculated width of the display in pixels with rotation applied. virtual int get_width() { return this->get_width_internal(); } diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 20549f049d..9ff393b397 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -1,21 +1,35 @@ +import importlib +import pkgutil + from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.mipi import flatten_sequence, map_sequence import esphome.config_validation as cv from esphome.const import ( CONF_BUSY_PIN, + CONF_CS_PIN, + CONF_DATA_RATE, CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_HEIGHT, CONF_ID, + CONF_INIT_SEQUENCE, CONF_LAMBDA, CONF_MODEL, - CONF_PAGES, CONF_RESET_DURATION, CONF_RESET_PIN, + CONF_WIDTH, ) +from . import models + AUTO_LOAD = ["split_buffer"] DEPENDENCIES = ["spi"] +CONF_INIT_SEQUENCE_ID = "init_sequence_id" + epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") EPaperBase = epaper_spi_ns.class_( "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer @@ -24,30 +38,79 @@ EPaperBase = epaper_spi_ns.class_( EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) -MODELS = { - "7.3in-spectra-e6": EPaper7p3InSpectraE6, -} +# Import all models dynamically from the models package +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) -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), +MODELS = models.EpaperModel.models + +DIMENSION_SCHEMA = cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } ) + +def model_schema(config): + model = MODELS[config[CONF_MODEL]] + class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required + return ( + display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), + ) + ) + .extend( + { + model.option(pin): pins.gpio_output_pin_schema + for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) + } + ) + .extend( + { + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + ) + + +def customise_schema(config): + """ + Create a customised config schema for a specific model and validate the configuration. + :param config: The configuration dictionary to validate + :return: The validated configuration dictionary + :raises cv.Invalid: If the configuration is invalid + """ + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + return model_schema(config)(config) + + +CONFIG_SCHEMA = customise_schema + FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( "epaper_spi", require_miso=False, require_mosi=True ) @@ -56,8 +119,23 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): model = MODELS[config[CONF_MODEL]] - rhs = model.new() - var = cg.Pvariable(config[CONF_ID], rhs, model) + init_sequence = config.get(CONF_INIT_SEQUENCE) + if init_sequence is None: + init_sequence = model.get_init_sequence(config) + init_sequence = flatten_sequence(init_sequence) + init_sequence_length = len(init_sequence) + init_sequence_id = cg.static_const_array( + config[CONF_INIT_SEQUENCE_ID], init_sequence + ) + width, height = model.get_dimensions(config) + var = cg.new_Pvariable( + config[CONF_ID], + model.name, + width, + height, + init_sequence_id, + init_sequence_length, + ) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 9630ea7f8b..cf6a0b0c3d 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -8,33 +8,20 @@ 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"); - } +static constexpr const char *const EPAPER_STATE_STRINGS[] = { + "IDLE", "UPDATE", "RESET", "RESET_END", + + "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", +}; + +const char *EPaperBase::epaper_state_to_string_() { + if (auto idx = static_cast(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) + return EPAPER_STATE_STRINGS[idx]; + return "Unknown"; } void EPaperBase::setup() { - if (!this->init_buffer_(this->get_buffer_length())) { + if (!this->init_buffer_(this->buffer_length_)) { this->mark_failed("Failed to initialise buffer"); return; } @@ -50,7 +37,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) { return true; } -void EPaperBase::setup_pins_() { +void EPaperBase::setup_pins_() const { this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); @@ -81,11 +68,7 @@ void EPaperBase::data(uint8_t value) { // 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; - +void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, format_hex_pretty(ptr, length, '.', false).c_str()); @@ -99,91 +82,146 @@ void EPaperBase::cmd_data(const uint8_t *data) { this->disable(); } -bool EPaperBase::is_idle_() { +bool EPaperBase::is_idle_() const { if (this->busy_pin_ == nullptr) { return true; } - return this->busy_pin_->digital_read(); + return !this->busy_pin_->digital_read(); } -void EPaperBase::reset() { +bool EPaperBase::reset_() const { 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(); }); - }); + if (this->state_ == EPaperState::RESET) { + this->reset_pin_->digital_write(false); + return false; + } + this->reset_pin_->digital_write(true); } + return true; } 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()))); + if (this->state_ != EPaperState::IDLE) { + ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); 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->set_state_(EPaperState::RESET); this->enable_loop(); } +void EPaperBase::wait_for_idle_(bool should_wait) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (should_wait) { + this->waiting_for_idle_start_ = millis(); + this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_; + } +#endif + this->waiting_for_idle_ = should_wait; +} + +/** + * Called during the loop task. + * First defer for any pending delays, then check if we are waiting for the display to become idle. + * If not waiting for idle, process the state machine. + */ + void EPaperBase::loop() { + auto now = millis(); + if (this->delay_until_ != 0) { + // using modulus arithmetic to handle wrap-around + int diff = now - this->delay_until_; + if (diff < 0) { + return; + } + this->delay_until_ = 0; + } if (this->waiting_for_idle_) { if (this->is_idle_()) { this->waiting_for_idle_ = false; + ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); } 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(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (now - this->waiting_for_idle_last_print_ >= 1000) { + ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); + this->waiting_for_idle_last_print_ = millis(); } +#endif return; } } + this->process_state_(); +} - auto state = this->state_queue_.front(); - - switch (state) { +/** + * Process the state machine. + * Typical state sequence: + * IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF -> + * DEEP_SLEEP -> IDLE + * + * Should a subclassed class need to override this, the method will need to be made virtual. + */ +void EPaperBase::process_state_() { + ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_()); + switch (this->state_) { + default: + ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->disable_loop(); + break; case EPaperState::IDLE: this->disable_loop(); break; + case EPaperState::RESET: + case EPaperState::RESET_END: + if (this->reset_()) { + this->set_state_(EPaperState::UPDATE); + } else { + this->set_state_(EPaperState::RESET_END); + } + break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda - break; - case EPaperState::RESET: - this->reset(); + this->set_state_(EPaperState::INITIALISE); break; case EPaperState::INITIALISE: this->initialise_(); + this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: if (!this->transfer_data()) { return; // Not done yet, come back next loop } + this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: this->power_on(); + this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: this->refresh_screen(); + this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: this->power_off(); + this->set_state_(EPaperState::DEEP_SLEEP); break; case EPaperState::DEEP_SLEEP: this->deep_sleep(); + this->set_state_(EPaperState::IDLE); break; } - this->state_queue_.pop(); +} + +void EPaperBase::set_state_(EPaperState state, uint16_t delay) { + ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_()); + this->state_ = state; + this->wait_for_idle_(state > EPaperState::SHOULD_WAIT); + if (delay != 0) { + this->delay_until_ = millis() + delay; + } else { + this->delay_until_ = 0; + } + ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, + TRUEFALSE(this->waiting_for_idle_)); } void EPaperBase::start_command_() { @@ -203,25 +241,39 @@ 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; + auto *sequence = this->init_sequence_; + auto length = this->init_sequence_length_; + while (index != length) { + if (length - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + const uint8_t cmd = sequence[index++]; + if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + const uint8_t num_args = x & 0x7F; + if (length - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args); + this->mark_failed(); + return; + } + ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args); + this->cmd_data(cmd, sequence + index, num_args); + index += num_args; + } } +} - this->power_on(); +void EPaperBase::dump_config() { + LOG_DISPLAY("", "E-Paper SPI", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->name_); + 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 diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index f6b2d41c65..4745ec7339 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -8,36 +8,48 @@ #include namespace esphome::epaper_spi { +using namespace display; enum class EPaperState : uint8_t { - IDLE, - UPDATE, - RESET, - INITIALISE, - TRANSFER_DATA, - POWER_ON, - REFRESH_SCREEN, - POWER_OFF, - DEEP_SLEEP, + IDLE, // not doing anything + UPDATE, // update the buffer + RESET, // drive reset low (active) + RESET_END, // drive reset high (inactive) + + SHOULD_WAIT, // states higher than this should wait for the display to be not busy + INITIALISE, // send the init sequence + TRANSFER_DATA, // transfer data to the display + POWER_ON, // power on the display + REFRESH_SCREEN, // send refresh command + POWER_OFF, // power off the display + DEEP_SLEEP, // deep sleep the display }; -static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t DELAY_FLAG = 0xFF; -class EPaperBase : public display::DisplayBuffer, +class EPaperBase : public DisplayBuffer, public spi::SPIDevice { public: - EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) - : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} + EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY) + : name_(name), + width_(width), + height_(height), + init_sequence_(init_sequence), + init_sequence_length_(init_sequence_length), + display_type_(display_type) {} 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 dump_config() override; void command(uint8_t value); void data(uint8_t value); - void cmd_data(const uint8_t *data); + void cmd_data(uint8_t command, const uint8_t *ptr, size_t length); void update() override; void loop() override; @@ -46,48 +58,84 @@ class EPaperBase : public display::DisplayBuffer, void on_safe_shutdown() override; + DisplayType get_display_type() override { return this->display_type_; }; + protected: - bool is_idle_(); - void setup_pins_(); - virtual void reset(); + int get_height_internal() override { return this->height_; }; + int get_width_internal() override { return this->width_; }; + void process_state_(); + + const char *epaper_state_to_string_(); + bool is_idle_() const; + void setup_pins_() const; + bool reset_() const; void initialise_(); + void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); virtual int get_width_controller() { return this->get_width_internal(); }; - virtual void deep_sleep() = 0; + + /** + * Methods that must be implemented by concrete classes to control the display + */ /** * Send data to the device via SPI - * @return true if done, false if should be called next loop + * @return true if done, false if it should be called next loop */ virtual bool transfer_data() = 0; + /** + * Refresh the screen after data transfer + */ virtual void refresh_screen() = 0; + /** + * Power the display on + */ virtual void power_on() = 0; + /** + * Power the display off + */ virtual void power_off() = 0; - virtual uint32_t get_buffer_length() = 0; + + /** + * Place the display into deep sleep + */ + virtual void deep_sleep() = 0; + + void set_state_(EPaperState state, uint16_t delay = 0); void start_command_(); void end_command_(); void start_data_(); void end_data_(); - const size_t init_sequence_length_{0}; + // properties initialised in the constructor + const char *name_; + uint16_t width_; + uint16_t height_; + const uint8_t *init_sequence_; + size_t init_sequence_length_; + DisplayType display_type_; - size_t current_data_index_{0}; + size_t buffer_length_{}; + size_t current_data_index_{0}; // used by data transfer to track progress uint32_t reset_duration_{200}; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t transfer_start_time_{}; uint32_t waiting_for_idle_last_print_{0}; + uint32_t waiting_for_idle_start_{0}; +#endif - GPIOPin *dc_pin_; - GPIOPin *busy_pin_{nullptr}; - GPIOPin *reset_pin_{nullptr}; - - const uint8_t *init_sequence_{nullptr}; + GPIOPin *dc_pin_{}; + GPIOPin *busy_pin_{}; + GPIOPin *reset_pin_{}; bool waiting_for_idle_{false}; + uint32_t delay_until_{0}; split_buffer::SplitBuffer buffer_; - std::queue state_queue_{{EPaperState::IDLE}}; + EPaperState state_{EPaperState::IDLE}; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp deleted file mode 100644 index f6273b392f..0000000000 --- a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#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 diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h deleted file mode 100644 index 6e850085ac..0000000000 --- a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h +++ /dev/null @@ -1,45 +0,0 @@ -#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 diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index dccc691252..8e4cbdde2a 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -1,135 +1,166 @@ #include "epaper_spi_spectra_e6.h" +#include + #include "esphome/core/log.h" namespace esphome::epaper_spi { - static constexpr const char *const TAG = "epaper_spi.6c"; +static constexpr size_t MAX_TRANSFER_SIZE = 128; +static constexpr unsigned char GRAY_THRESHOLD = 50; -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 - } +enum E6Color { + BLACK, + WHITE, + YELLOW, + RED, + SKIP_1, + BLUE, + GREEN, + CYAN, + SKIP_2, +}; + +static uint8_t color_to_hex(Color color) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + unsigned char max_rgb = std::max({color.r, color.g, color.b}); + unsigned char min_rgb = std::min({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast(color.r) + color.g + color.b) > 382) { + return WHITE; } + return BLACK; } + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + bool r_on = (color.r > 128); + bool g_on = (color.g > 128); + bool b_on = (color.b > 128); + + if (r_on && g_on && !b_on) { + return YELLOW; + } + if (r_on && !g_on && !b_on) { + return RED; + } + if (!r_on && g_on && !b_on) { + return GREEN; + } + if (!r_on && !g_on && b_on) { + return BLUE; + } + // Handle "impure" colors (Cyan, Magenta) + if (!r_on && g_on && b_on) { + // Cyan (G+B) -> Closest is Green or Blue. Pick Green. + return GREEN; + } + if (r_on && !g_on) { + // Magenta (R+B) -> Closest is Red or Blue. Pick Red. + return RED; + } + // Handle the remaining corners (White-ish, Black-ish) + if (r_on) { + // All high (but not gray) -> White + return WHITE; + } + // !r_on && !g_on && !b_on + // All low (but not gray) -> Black + return BLACK; +} + +void EPaperSpectraE6::power_on() { + ESP_LOGD(TAG, "Power on"); + this->command(0x04); +} + +void EPaperSpectraE6::power_off() { + ESP_LOGD(TAG, "Power off"); + this->command(0x02); + this->data(0x00); +} + +void EPaperSpectraE6::refresh_screen() { + ESP_LOGD(TAG, "Refresh"); + this->command(0x12); + this->data(0x00); +} + +void EPaperSpectraE6::deep_sleep() { + ESP_LOGD(TAG, "Deep sleep"); + this->command(0x07); + this->data(0xA5); } void EPaperSpectraE6::fill(Color color) { - uint8_t pixel_color; - if (color.is_on()) { - pixel_color = color_to_hex(color); - } else { - pixel_color = 0x1; - } + auto pixel_color = color_to_hex(color); - // 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; - } + // We store 2 pixels per byte + this->buffer_.fill(pixel_color + (pixel_color << 4)); } -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 EPaperSpectraE6::clear() { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); } 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) + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) return; - uint8_t pixel_bits = color_to_hex(color); + auto 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)); + uint32_t byte_position = pixel_position / 2; + auto original = this->buffer_[byte_position]; + if ((pixel_position & 1) != 0) { + this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; } 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)); + this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); } } bool HOT EPaperSpectraE6::transfer_data() { const uint32_t start_time = App.get_loop_component_start_time(); + const size_t buffer_length = this->buffer_length_; if (this->current_data_index_ == 0) { - ESP_LOGV(TAG, "Sending data"); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + this->transfer_start_time_ = millis(); +#endif + ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis()); 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); + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; - this->start_data_(); - this->write_array(bytes_to_send, sizeof(bytes_to_send)); - this->end_data_(); + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; - 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; + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } } } // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + } this->current_data_index_ = 0; + ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_); 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 diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index 9f0652f79d..48356ad74b 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -6,18 +6,23 @@ 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) {} + EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { + this->buffer_length_ = width * height / 2; // 2 pixels per byte + } - display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } void fill(Color color) override; + void clear() override; protected: + void refresh_screen() override; + void power_on() override; + void power_off() override; + void deep_sleep() override; 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 diff --git a/esphome/components/epaper_spi/models/__init__.py b/esphome/components/epaper_spi/models/__init__.py new file mode 100644 index 0000000000..019eb31d18 --- /dev/null +++ b/esphome/components/epaper_spi/models/__init__.py @@ -0,0 +1,65 @@ +from typing import Any, Self + +import esphome.config_validation as cv +from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH + + +class EpaperModel: + models: dict[str, Self] = {} + + def __init__( + self, + name: str, + class_name: str, + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.class_name = class_name + self.initsequence = initsequence + self.defaults = defaults + EpaperModel.models[name] = self + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + def get_init_sequence(self, config: dict): + return self.initsequence + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + if fallback is None and self.get_default(name, None) is None: + return cv.Required(name) + return cv.Optional(name, default=self.get_default(name, fallback)) + + def get_dimensions(self, config) -> tuple[int, int]: + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + else: + (width, height) = dimensions + + else: + # Default dimensions, use model defaults + width = self.get_default(CONF_WIDTH) + height = self.get_default(CONF_HEIGHT) + return width, height + + def extend(self, name, **kwargs) -> "EpaperModel": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence) or ()) + initsequence.extend(kwargs.pop("add_init_sequence", ())) + defaults = self.defaults.copy() + defaults.update(kwargs) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py new file mode 100644 index 0000000000..9f0b673d69 --- /dev/null +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -0,0 +1,51 @@ +from typing import Any + +from . import EpaperModel + + +class SpectraE6(EpaperModel): + def __init__(self, name, class_name="EPaperSpectraE6", **kwargs): + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, height = self.get_dimensions(config) + return ( + (0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,), + (0x01, 0x3F,), + (0x00, 0x5F, 0x69,), + (0x03, 0x00, 0x54, 0x00, 0x44,), + (0x05, 0x40, 0x1F, 0x1F, 0x2C,), + (0x06, 0x6F, 0x1F, 0x17, 0x49,), + (0x08, 0x6F, 0x1F, 0x1F, 0x22,), + (0x30, 0x03,), + (0x50, 0x3F,), + (0x60, 0x02, 0x00,), + (0x61, width // 256, width % 256, height // 256, height % 256,), + (0x84, 0x01,), + (0xE3, 0x2F,), + ) + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + +spectra_e6 = SpectraE6("spectra-e6") + +spectra_e6.extend( + "Seeed-reTerminal-E1002", + width=800, + height=480, + data_rate="20MHz", + cs_pin=10, + dc_pin=11, + reset_pin=12, + busy_pin={ + "number": 13, + "inverted": True, + "mode": { + "input": True, + "pullup": True, + }, + }, +) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 93d1750cd6..4dbc81caa2 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -218,6 +218,21 @@ def map_sequence(value): return tuple(value) +def flatten_sequence(sequence: tuple | list): + """ + Flatten an init sequence into a single list of bytes. + :param sequence: The list of tuples + :return: a list of bytes + """ + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ) + + def delay(ms): return DELAY_FLAG, ms @@ -456,13 +471,7 @@ class DriverChip: # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed - return sum( - tuple( - (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] - for x in sequence - ), - (), - ), madctl + return flatten_sequence(sequence), madctl def requires_buffer(config) -> bool: diff --git a/esphome/components/split_buffer/split_buffer.cpp b/esphome/components/split_buffer/split_buffer.cpp index a710670a5d..526a19c71c 100644 --- a/esphome/components/split_buffer/split_buffer.cpp +++ b/esphome/components/split_buffer/split_buffer.cpp @@ -4,7 +4,6 @@ #include "esphome/core/log.h" namespace esphome::split_buffer { - static constexpr const char *const TAG = "split_buffer"; SplitBuffer::~SplitBuffer() { this->free(); } @@ -102,32 +101,44 @@ void SplitBuffer::free() { this->total_length_ = 0; } -uint8_t &SplitBuffer::operator[](size_t index) { +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 + // Return reference to a static dummy byte since we can't throw exceptions. + // the byte is non-const since it will also be used by the non-const [] overload. 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; + const auto buffer_index = index / this->buffer_size_; + const auto offset_in_buffer = index % this->buffer_size_; 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; +// non-const version of operator[] for write access +uint8_t &SplitBuffer::operator[](size_t index) { + // avoid code duplication. These casts are safe since we know the object is not const. + return const_cast(static_cast(this)->operator[](index)); +} + +/** + * Fill the entire buffer with a single byte value + * @param value Fill value + */ +void SplitBuffer::fill(uint8_t value) const { + if (this->buffer_count_ == 0) + return; + // clear all the full sized buffers + size_t i = 0; + for (; i != this->buffer_count_ - 1; i++) { + memset(this->buffers_[i], value, this->buffer_size_); } - - 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]; + // clear the last, potentially short, buffer. + // `i` is guaranteed to equal the last index since the loop terminates at that value. + // where all buffers are the same size, the modulus must return the size, not 0. + auto size_last = ((this->total_length_ - 1) % this->buffer_size_) + 1; + memset(this->buffers_[i], value, size_last); } } // namespace esphome::split_buffer diff --git a/esphome/components/split_buffer/split_buffer.h b/esphome/components/split_buffer/split_buffer.h index c3490f3d6e..b615ddce74 100644 --- a/esphome/components/split_buffer/split_buffer.h +++ b/esphome/components/split_buffer/split_buffer.h @@ -4,7 +4,13 @@ #include namespace esphome::split_buffer { - +/** + * A SplitBuffer allocates a large memory buffer potentially as multiple smaller buffers + * to facilitate allocation of large buffers on devices with fragmented memory spaces. + * Each sub-buffer is the same size, except for the last one which may be smaller. + * Standard array indexing using `[]` is possible on the buffer, but, since the buffer may not be contiguous in memory, + * there is no easy way to access the buffer as a single array, i.e. no `.data()` access like a vector. + */ class SplitBuffer { public: SplitBuffer() = default; @@ -19,13 +25,13 @@ class SplitBuffer { // Access operators uint8_t &operator[](size_t index); const uint8_t &operator[](size_t index) const; + void fill(uint8_t value) 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; } diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 34aefb82b4..cff1f51897 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -4,7 +4,10 @@ packages: display: - platform: epaper_spi spi_id: spi_bus - model: 7.3in-spectra-e6 + model: spectra-e6 + dimensions: + width: 800 + height: 480 cs_pin: GPIO5 dc_pin: GPIO17 reset_pin: GPIO16 @@ -13,3 +16,6 @@ display: update_interval: 60s lambda: |- it.circle(64, 64, 50, Color::BLACK); + + - platform: epaper_spi + model: seeed-reterminal-e1002 From 99ce989eaedd59fdad9ea28a0468d63d31568d95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Nov 2025 16:30:35 -0600 Subject: [PATCH 2/2] [micro_wake_word] Add wake_loop_threadsafe() for low-latency wake word detection (#11698) --- esphome/components/micro_wake_word/__init__.py | 7 ++++++- esphome/components/micro_wake_word/micro_wake_word.cpp | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 8cd7115368..575fb97799 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone +from esphome.components import esp32, microphone, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] +AUTO_LOAD = ["socket"] DOMAIN = "micro_wake_word" @@ -443,6 +444,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Enable wake_loop_threadsafe() for low-latency wake word detection + # The inference task queues detection events that need immediate processing + socket.require_wake_loop_threadsafe() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 6fca48a5bd..a0547b158e 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -426,6 +427,12 @@ void MicroWakeWord::process_probabilities_() { if (vad_state.detected) { #endif xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); + + // Wake main loop immediately to process wake word detection +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD } else {