diff --git a/CODEOWNERS b/CODEOWNERS index d5b81d548e..03ea5d0e47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/epaper_spi/* @esphome/core esphome/components/es7210/* @kahrendt esphome/components/es7243e/* @kbx81 esphome/components/es8156/* @kbx81 @@ -429,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow +esphome/components/split_buffer/* @jesserockz esphome/components/sprinkler/* @kbx81 esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 diff --git a/esphome/components/epaper_spi/__init__.py b/esphome/components/epaper_spi/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/epaper_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py new file mode 100644 index 0000000000..20549f049d --- /dev/null +++ b/esphome/components/epaper_spi/display.py @@ -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])) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp new file mode 100644 index 0000000000..21be4a2c05 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -0,0 +1,227 @@ +#include "epaper_spi.h" +#include +#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 diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h new file mode 100644 index 0000000000..f6b2d41c65 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -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 + +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 { + 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 state_queue_{{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 new file mode 100644 index 0000000000..f6273b392f --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp @@ -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 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 new file mode 100644 index 0000000000..6e850085ac --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h @@ -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 diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp new file mode 100644 index 0000000000..dccc691252 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -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 diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h new file mode 100644 index 0000000000..9f0652f79d --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -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 diff --git a/esphome/components/split_buffer/__init__.py b/esphome/components/split_buffer/__init__.py new file mode 100644 index 0000000000..be7472936f --- /dev/null +++ b/esphome/components/split_buffer/__init__.py @@ -0,0 +1,5 @@ +CODEOWNERS = ["@jesserockz"] + +# Allows split_buffer to be configured in yaml, to allow use of the C++ api. + +CONFIG_SCHEMA = {} diff --git a/esphome/components/split_buffer/split_buffer.cpp b/esphome/components/split_buffer/split_buffer.cpp new file mode 100644 index 0000000000..a710670a5d --- /dev/null +++ b/esphome/components/split_buffer/split_buffer.cpp @@ -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 ptr_allocator; + RAMAllocator 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 allocator; + for (size_t i = 0; i < this->buffer_count_; i++) { + if (this->buffers_[i] != nullptr) { + allocator.deallocate(this->buffers_[i], 0); + } + } + RAMAllocator 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 diff --git a/esphome/components/split_buffer/split_buffer.h b/esphome/components/split_buffer/split_buffer.h new file mode 100644 index 0000000000..c3490f3d6e --- /dev/null +++ b/esphome/components/split_buffer/split_buffer.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +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 diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..3d8d62a7ca --- /dev/null +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -0,0 +1,15 @@ +spi: + clk_pin: GPIO7 + mosi_pin: GPIO9 + +display: + - platform: epaper_spi + model: 7.3in-spectra-e6 + cs_pin: GPIO5 + dc_pin: GPIO17 + reset_pin: GPIO16 + busy_pin: GPIO4 + rotation: 0 + update_interval: 60s + lambda: |- + it.circle(64, 64, 50, Color::BLACK);