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..8b6ddf124c --- /dev/null +++ b/esphome/components/epaper_spi/display.py @@ -0,0 +1,99 @@ +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_FULL_UPDATE_EVERY, + 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 +) +EPaper6Color = epaper_spi_ns.class_("EPaper6Color", EPaperBase) +EPaper7p3InE = epaper_spi_ns.class_("EPaper7p3InE", EPaper6Color) + +MODELS = { + "7.30in-e": ("b", EPaper7p3InE), +} + + +def validate_full_update_every_only_types_ac(value): + if CONF_FULL_UPDATE_EVERY not in value: + return value + if MODELS[value[CONF_MODEL]][0] == "b": + full_models = [] + for key, val in sorted(MODELS.items()): + if val[0] != "b": + full_models.append(key) + raise cv.Invalid( + "The 'full_update_every' option is only available for models " + + ", ".join(full_models) + ) + return value + + +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), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_FULL_UPDATE_EVERY): cv.int_range(min=1, max=4294967295), + cv.Optional(CONF_RESET_DURATION): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + .extend(cv.polling_component_schema("1s")) + .extend(spi.spi_device_schema()), + validate_full_update_every_only_types_ac, + 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: + reset = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + cg.add(var.set_busy_pin(reset)) + if CONF_FULL_UPDATE_EVERY in config: + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + 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..9d8005560d --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -0,0 +1,362 @@ +#include "epaper_spi.h" +#include +#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 uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run + +void EPaperBase::setup() { + this->init_internal_(this->get_buffer_length()); + this->setup_pins_(); + this->spi_setup(); +} + +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 one or more bytes of data. +// The command is the first byte, length is the total including cmd. +void EPaperBase::cmd_data(const uint8_t *c_data, size_t length) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(c_data[0]); + this->dc_pin_->digital_write(true); + this->write_array(c_data + 1, length - 1); + 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->state_ = RESET_DONE; + this->enable_loop(); + }); + }); + } +} + +void EPaperBase::update() { + if (this->state_ != IDLE) { + ESP_LOGE(TAG, "Display update already in progress"); + return; + } + this->do_update_(); // Calls ESPHome (current page) lambda + this->state_ = UPDATING; + this->enable_loop(); +} + +void EPaperBase::loop() { + switch (this->state_) { + case IDLE: + this->disable_loop(); + break; + case UPDATING: + this->reset_(); + this->state_ = RESETTING; + break; + case RESETTING: + // Nothing to do here, next state change is handled by timeouts + break; + case RESET_DONE: + if (this->is_idle_()) { + this->initialize(); + this->state_ = INITIALIZING; + } + break; + case INITIALIZING: + if (this->is_idle_()) { + ESP_LOGI(TAG, "Display initialized successfully"); + this->state_ = TRANSFERING_DATA; + } + break; + case TRANSFERING_DATA: + this->transfer_data(); + break; + case TRANSFER_DONE: + this->power_on(); + this->state_ = POWERING_ON; + break; + case POWERING_ON: + if (this->is_idle_()) { + this->refresh_screen(); + this->state_ = REFRESHING_SCREEN; + } + break; + case REFRESHING_SCREEN: + if (this->is_idle_()) { + this->power_off(); + this->state_ = POWERING_OFF; + } + break; + case POWERING_OFF: + if (this->is_idle_()) { + this->deep_sleep(); + } + this->state_ = IDLE; + this->disable_loop(); + break; + } +} + +void EPaper6Color::setup() { + if (!this->init_internal_6c_(this->get_buffer_length())) { + this->mark_failed("Failed to initialize buffer"); + return; + } + this->setup_pins_(); + this->spi_setup(); +} + +bool EPaper6Color::init_internal_6c_(uint32_t buffer_length) { + if (!this->buffer_.init(buffer_length)) { + return false; + } + this->clear(); + return true; +} + +uint8_t EPaper6Color::color_to_hex(Color color) { + uint8_t hex_code; + if (color.red > 127) { + if (color.green > 170) { + if (color.blue > 127) { + hex_code = 0x1; // White + } else { + hex_code = 0x2; // Yellow + } + } else { + hex_code = 0x3; // Red (or Magenta) + } + } else { + if (color.green > 127) { + if (color.blue > 127) { + hex_code = 0x5; // Cyan -> Blue + } else { + hex_code = 0x6; // Green + } + } else { + if (color.blue > 127) { + hex_code = 0x5; // Blue + } else { + hex_code = 0x0; // Black + } + } + } + + return hex_code; +} +void EPaper6Color::fill(Color color) { + uint8_t pixel_color; + if (color.is_on()) { + pixel_color = this->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 EPaper6Color::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 EPaper6Color::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 = this->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)); + } +} + +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 EPaper7p3InE::initialize() { + static const uint8_t cmdh_data[] = {0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18}; + this->cmd_data(cmdh_data, sizeof(cmdh_data)); + + static const uint8_t data_01[] = {0x01, 0x3F}; + this->cmd_data(data_01, sizeof(data_01)); + + static const uint8_t data_02[] = {0x00, 0x5F, 0x69}; + this->cmd_data(data_02, sizeof(data_02)); + + static const uint8_t data_03[] = {0x03, 0x00, 0x54, 0x00, 0x44}; + this->cmd_data(data_03, sizeof(data_03)); + + static const uint8_t data_04[] = {0x05, 0x40, 0x1F, 0x1F, 0x2C}; + this->cmd_data(data_04, sizeof(data_04)); + + static const uint8_t data_05[] = {0x06, 0x6F, 0x1F, 0x17, 0x49}; + this->cmd_data(data_05, sizeof(data_05)); + + static const uint8_t data_06[] = {0x08, 0x6F, 0x1F, 0x1F, 0x22}; + this->cmd_data(data_06, sizeof(data_06)); + + static const uint8_t data_07[] = {0x30, 0x03}; + this->cmd_data(data_07, sizeof(data_07)); + + static const uint8_t data_08[] = {0x50, 0x3F}; + this->cmd_data(data_08, sizeof(data_08)); + + static const uint8_t data_09[] = {0x60, 0x02, 0x00}; + this->cmd_data(data_09, sizeof(data_09)); + + static const uint8_t data_10[] = {0x61, 0x03, 0x20, 0x01, 0xE0}; + this->cmd_data(data_10, sizeof(data_10)); + + static const uint8_t data_11[] = {0x84, 0x01}; + this->cmd_data(data_11, sizeof(data_11)); + + static const uint8_t data_12[] = {0xE3, 0x2F}; + this->cmd_data(data_12, sizeof(data_12)); + + this->power_on(); +} + +void HOT EPaper7p3InE::transfer_data() { + const uint32_t start_time = App.get_loop_component_start_time(); + if (this->current_data_index_ == 0) { + ESP_LOGI(TAG, "Sending data to the display"); + 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) { + std::bitset<24> triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); + // 8 bitset<3> are stored in 3 bytes + // |aaabbbaa|abbbaaab|bbaaabbb| + // | byte 1 | byte 2 | byte 3 | + bytes_to_send[0] = ((triplet >> 17).to_ulong() & 0b01110000) | ((triplet >> 18).to_ulong() & 0b00000111); + bytes_to_send[1] = ((triplet >> 11).to_ulong() & 0b01110000) | ((triplet >> 12).to_ulong() & 0b00000111); + bytes_to_send[2] = ((triplet >> 5).to_ulong() & 0b01110000) | ((triplet >> 6).to_ulong() & 0b00000111); + bytes_to_send[3] = ((triplet << 1).to_ulong() & 0b01110000) | ((triplet << 0).to_ulong() & 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; + } + } + // Finished the entire dataset + this->current_data_index_ = 0; + this->state_ = TRANSFER_DONE; +} + +void EPaper7p3InE::power_on() { + ESP_LOGI(TAG, "Power on the display"); + this->command(0x04); +} + +void EPaper7p3InE::power_off() { + ESP_LOGI(TAG, "Power off the display"); + this->command(0x02); + this->data(0x00); +} + +void EPaper7p3InE::refresh_screen() { + this->command(0x12); + this->data(0x00); +} + +void EPaper7p3InE::deep_sleep() { + ESP_LOGI(TAG, "Set the display to deep sleep"); + this->command(0x07); + this->data(0xA5); +} + +void EPaper7p3InE::dump_config() { + LOG_DISPLAY("", "E-Paper SPI", this); + ESP_LOGCONFIG(TAG, " Model: 7.3in-E"); + 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 new file mode 100644 index 0000000000..92b310f39e --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -0,0 +1,109 @@ +#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" + +namespace esphome::epaper_spi { + +enum EPaperState : uint8_t { + IDLE, + UPDATING, + RESETTING, + RESET_DONE, + INITIALIZING, + TRANSFERING_DATA, + TRANSFER_DONE, + POWERING_ON, + REFRESHING_SCREEN, + POWERING_OFF, +}; + +class EPaperBase : public display::DisplayBuffer, + public spi::SPIDevice { + public: + 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, size_t length); + + void update() override; + void loop() override; + + void setup() override; + + void on_safe_shutdown() override; + + protected: + bool is_idle_(); + void setup_pins_(); + void reset_(); + + virtual int get_width_controller() { return this->get_width_internal(); }; + virtual void initialize() = 0; + virtual void deep_sleep() = 0; + virtual void 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_(); + + GPIOPin *dc_pin_; + GPIOPin *busy_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + + uint32_t reset_duration_{200}; + + EPaperState state_{IDLE}; + + split_buffer::SplitBuffer buffer_; +}; + +class EPaper6Color : public EPaperBase { + public: + uint8_t color_to_hex(Color color); + void fill(Color color) override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + uint32_t get_buffer_length() override; + void setup() override; + bool init_internal_6c_(uint32_t buffer_length); +}; + +class EPaper7p3InE : public EPaper6Color { + public: + void initialize() override; + + void dump_config() override; + + protected: + int get_width_internal() override { return 800; }; + int get_height_internal() override { return 480; }; + void transfer_data() override; + void refresh_screen() override; + + void power_on() override; + void power_off() override; + + void deep_sleep() override; + + size_t current_data_index_{0}; +}; + +} // 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..41b5484589 --- /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, halve the buffer size + current_buffer_size = current_buffer_size / 2; + continue; + } + + // 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, "SplitBuffer allocated %zu buffers of %zu bytes each (total: %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, "SplitBuffer 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, "SplitBuffer index %zu out of bounds (size: %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_; + + 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, "SplitBuffer index %zu out of bounds (size: %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_; + + 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..12729c0066 --- /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 total_length_; } + + // Get buffer information + size_t get_buffer_count() const { return buffer_count_; } + size_t get_buffer_size() const { return buffer_size_; } + + // Check if successfully initialized + bool is_valid() const { return buffers_ != nullptr && 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