diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 1451d14e2e..ebc3c0a9f6 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -7,7 +7,6 @@ namespace esphome { namespace display { - static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); @@ -16,6 +15,7 @@ const Color COLOR_ON(255, 255, 255, 255); void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } + void HOT Display::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; @@ -91,23 +91,27 @@ void HOT Display::horizontal_line(int x, int y, int width, Color color) { for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } + void HOT Display::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } + void Display::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } + void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } + void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; @@ -131,6 +135,7 @@ void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { } } while (dx <= 0); } + void Display::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; @@ -157,6 +162,7 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } } while (dx <= 0); } + void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -213,6 +219,7 @@ void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -228,7 +235,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, // outer dots this->draw_pixel_at(center_x + dxmax, center_y - dymax, color); this->draw_pixel_at(center_x - dxmax, center_y - dymax, color); - if (dymin < rmin) { // side parts + if (dymin < rmin) { + // side parts int lhline_width = -(dxmax - dxmin) + 1; if (progress >= 50) { if (float(dymax) < float(-dxmax) * tan_a) { @@ -239,7 +247,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left if (!dymax) this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border - if (upd_dxmax > -dxmin) { // right + if (upd_dxmax > -dxmin) { + // right int rhline_width = (upd_dxmax + dxmin) + 1; this->horizontal_line(center_x - dxmin, center_y - dymax, rhline_width > lhline_width ? lhline_width : rhline_width, color); @@ -256,7 +265,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, if (lhline_width > 0) this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); } - } else { // top part + } else { + // top part int hline_width = 2 * (-dxmax) + 1; if (progress >= 50) { if (dymax < float(-dxmax) * tan_a) { @@ -300,11 +310,13 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { this->line(x1, y1, x2, y2, color); this->line(x1, y1, x3, y3, color); this->line(x2, y2, x3, y3, color); } + void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { if (*y1 > *y2) { int x_temp = *x1, y_temp = *y1; @@ -322,6 +334,7 @@ void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3 = x_temp, *y3 = y_temp; } } + void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // y2 must be equal to y3 (same horizontal line) @@ -333,7 +346,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s1_dy = abs(y2 - y1); int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1; int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1; - if (s1_dy > s1_dx) { // swap values + if (s1_dy > s1_dx) { + // swap values int tmp = s1_dx; s1_dx = s1_dy; s1_dy = tmp; @@ -349,7 +363,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s2_dy = abs(y3 - y1); int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1; int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1; - if (s2_dy > s2_dx) { // swap values + if (s2_dy > s2_dx) { + // swap values int tmp = s2_dx; s2_dx = s2_dy; s2_dy = tmp; @@ -402,20 +417,25 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, } } } + void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3); - if (y2 == y3) { // Check for special case of a bottom-flat triangle + if (y2 == y3) { + // Check for special case of a bottom-flat triangle this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color); - } else if (y1 == y2) { // Check for special case of a top-flat triangle + } else if (y1 == y2) { + // Check for special case of a top-flat triangle this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color); - } else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle + } else { + // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2; this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color); this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); } } + void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees) { @@ -447,7 +467,8 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo int current_vertex_x, current_vertex_y; get_regular_polygon_vertex(current_vertex_id, ¤t_vertex_x, ¤t_vertex_y, x, y, radius, edges, variation, rotation_degrees); - if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated + if (current_vertex_id > 0) { + // Start drawing after the 2nd vertex coordinates has been calculated if (drawing == DRAWING_FILLED) { this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); } else if (drawing == DRAWING_OUTLINE) { @@ -459,21 +480,26 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo } } } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing); } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees, Color color) { regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); } @@ -584,15 +610,19 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te break; } } + void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background); } + void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { this->print(x, y, font, COLOR_ON, align, text); } + void Display::print(int x, int y, BaseFont *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } + void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) { va_list arg; @@ -600,31 +630,37 @@ void Display::printf(int x, int y, BaseFont *font, Color color, Color background this->vprintf_(x, y, font, color, background, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } + void Display::set_pages(std::vector pages) { for (auto *page : pages) page->set_parent(this); @@ -637,6 +673,7 @@ void Display::set_pages(std::vector pages) { pages[pages.size() - 1]->set_next(pages[0]); this->show_page(pages[0]); } + void Display::show_page(DisplayPage *page) { this->previous_page_ = this->page_; this->page_ = page; @@ -645,8 +682,10 @@ void Display::show_page(DisplayPage *page) { t->process(this->previous_page_, this->page_); } } + void Display::show_next_page() { this->page_->show_next(); } void Display::show_prev_page() { this->page_->show_prev(); } + void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); @@ -660,10 +699,12 @@ void Display::do_update_() { } this->clear_clipping_(); } + void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } + void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ESPTime time) { char buffer[64]; @@ -671,15 +712,19 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou if (ret > 0) this->print(x, y, font, color, align, buffer, background); } + void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } + void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } @@ -691,6 +736,7 @@ void Display::start_clipping(Rect rect) { } this->clipping_rectangle_.push_back(rect); } + void Display::end_clipping() { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "clear: Clipping is not set."); @@ -698,6 +744,7 @@ void Display::end_clipping() { this->clipping_rectangle_.pop_back(); } } + void Display::extend_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -705,6 +752,7 @@ void Display::extend_clipping(Rect add_rect) { this->clipping_rectangle_.back().extend(add_rect); } } + void Display::shrink_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -712,6 +760,7 @@ void Display::shrink_clipping(Rect add_rect) { this->clipping_rectangle_.back().shrink(add_rect); } } + Rect Display::get_clipping() const { if (this->clipping_rectangle_.empty()) { return Rect(); @@ -719,7 +768,9 @@ Rect Display::get_clipping() const { return this->clipping_rectangle_.back(); } } + void Display::clear_clipping_() { this->clipping_rectangle_.clear(); } + bool Display::clip(int x, int y) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) return false; @@ -727,6 +778,7 @@ bool Display::clip(int x, int y) { return false; return true; } + bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { min_x = std::max(x, 0); max_x = std::min(x + w, this->get_width()); @@ -742,6 +794,7 @@ bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { return min_x < max_x; } + bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { min_y = std::max(y, 0); max_y = std::min(y + h, this->get_height()); @@ -766,15 +819,15 @@ void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; this->clear(); this->show_test_card_ = false; + image_w = std::min(w - 20, 310); + image_h = std::min(h - 20, 255); + int shift_x = (w - image_w) / 2; + int shift_y = (h - image_h) / 2; + int line_w = (image_w - 6) / 6; + int image_c = image_w / 2; if (this->get_display_type() == DISPLAY_TYPE_COLOR) { Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255); - image_w = std::min(w - 20, 310); - image_h = std::min(h - 20, 255); - int shift_x = (w - image_w) / 2; - int shift_y = (h - image_h) / 2; - int line_w = (image_w - 6) / 6; - int image_c = image_w / 2; for (auto i = 0; i != image_h; i++) { int c = esp_scale(i, image_h); this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c)); @@ -786,26 +839,26 @@ void Display::test_card() { this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c)); this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c)); } - this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); + } + this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); - uint16_t shift_r = shift_x + line_w - (8 * 3); - uint16_t shift_g = shift_x + image_c - (8 * 3); - uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); - shift_y = h / 2 - (8 * 3); - for (auto i = 0; i < 8; i++) { - uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); - uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); - uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); - for (auto k = 0; k < 8; k++) { - if ((ftr & (1 << k)) != 0) { - this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftg & (1 << k)) != 0) { - this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftb & (1 << k)) != 0) { - this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } + uint16_t shift_r = shift_x + line_w - (8 * 3); + uint16_t shift_g = shift_x + image_c - (8 * 3); + uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); + shift_y = h / 2 - (8 * 3); + for (auto i = 0; i < 8; i++) { + uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); + uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); + uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); + for (auto k = 0; k < 8; k++) { + if ((ftr & (1 << k)) != 0) { + this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftg & (1 << k)) != 0) { + this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftb & (1 << k)) != 0) { + this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); } } } @@ -818,7 +871,9 @@ void Display::test_card() { } DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} + void DisplayPage::show() { this->parent_->show_page(this); } + void DisplayPage::show_next() { if (this->next_ == nullptr) { ESP_LOGE(TAG, "no next page"); @@ -826,6 +881,7 @@ void DisplayPage::show_next() { } this->next_->show(); } + void DisplayPage::show_prev() { if (this->prev_ == nullptr) { ESP_LOGE(TAG, "no previous page"); @@ -833,6 +889,7 @@ void DisplayPage::show_prev() { } this->prev_->show(); } + void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } @@ -868,6 +925,5 @@ const LogString *text_align_to_string(TextAlign textalign) { return LOG_STR("UNKNOWN"); } } - } // namespace display } // namespace esphome diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 182c37ba40..ff5693c206 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -4,8 +4,10 @@ import pkgutil from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation from esphome.components.mipi import flatten_sequence, map_sequence import esphome.config_validation as cv +from esphome.config_validation import update_interval from esphome.const import ( CONF_BUSY_PIN, CONF_CS_PIN, @@ -13,15 +15,25 @@ from esphome.const import ( CONF_DC_PIN, CONF_DIMENSIONS, CONF_ENABLE_PIN, + CONF_FULL_UPDATE_EVERY, CONF_HEIGHT, CONF_ID, CONF_INIT_SEQUENCE, CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, CONF_MODEL, + CONF_PAGES, CONF_RESET_DURATION, CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_UPDATE_INTERVAL, CONF_WIDTH, ) +from esphome.cpp_generator import RawExpression +from esphome.final_validate import full_config from . import models @@ -32,8 +44,9 @@ 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 + "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.Display ) +Transform = epaper_spi_ns.enum("Transform") EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) @@ -52,6 +65,8 @@ DIMENSION_SCHEMA = cv.Schema( } ) +TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + def model_schema(config): model = MODELS[config[CONF_MODEL]] @@ -73,7 +88,18 @@ def model_schema(config): ) .extend( { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional( + CONF_UPDATE_INTERVAL, default=cv.UNDEFINED + ): update_interval, + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), 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), @@ -111,9 +137,29 @@ def customise_schema(config): CONFIG_SCHEMA = customise_schema -FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( - "epaper_spi", require_miso=False, require_mosi=True -) + +def _final_validate(config): + spi.final_validate_device_schema( + "epaper_spi", require_miso=False, require_mosi=True + )(config) + + global_config = full_config.get() + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if CONF_LAMBDA not in config and CONF_PAGES not in config: + if LVGL_DOMAIN in global_config: + if CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("never") + else: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + config[CONF_UPDATE_INTERVAL] = core.TimePeriod( + seconds=60 + ).total_milliseconds + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): @@ -137,7 +183,9 @@ async def to_code(config): init_sequence_length, ) - await display.register_display(var, config) + # Rotation is handled by setting the transform + display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} + await display.register_display(var, display_config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) @@ -148,11 +196,35 @@ async def to_code(config): 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]) + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(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]) + if busy_pin := config.get(CONF_BUSY_PIN): + busy = await cg.gpio_pin_expression(busy_pin) cg.add(var.set_busy_pin(busy)) + 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])) + if transform := config.get(CONF_TRANSFORM): + transform[CONF_SWAP_XY] = False + else: + transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS} + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + elif rotation == 270: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform_str = "|".join( + { + str(getattr(Transform, x.upper())) + for x in TRANSFORM_OPTIONS + if transform.get(x) + } + ) + if transform_str: + cg.add(var.set_transform(RawExpression(transform_str))) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 39959cd743..f6313d33ef 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -9,9 +9,8 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; 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", + "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_() { @@ -69,8 +68,8 @@ void EPaperBase::data(uint8_t value) { // 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(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()); + ESP_LOGV(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(); @@ -89,7 +88,7 @@ bool EPaperBase::is_idle_() const { return !this->busy_pin_->digital_read(); } -bool EPaperBase::reset_() const { +bool EPaperBase::reset() { if (this->reset_pin_ != nullptr) { if (this->state_ == EPaperState::RESET) { this->reset_pin_->digital_write(false); @@ -105,16 +104,16 @@ void EPaperBase::update() { ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); return; } - this->set_state_(EPaperState::RESET); + this->set_state_(EPaperState::UPDATE); this->enable_loop(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + this->update_start_time_ = millis(); +#endif } 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_; - } + this->waiting_for_idle_start_ = millis(); #endif this->waiting_for_idle_ = should_wait; } @@ -138,7 +137,9 @@ void EPaperBase::loop() { 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_)); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); +#endif } else { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE if (now - this->waiting_for_idle_last_print_ >= 1000) { @@ -164,23 +165,27 @@ 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(); + ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->set_state_(EPaperState::IDLE); break; case EPaperState::IDLE: this->disable_loop(); break; case EPaperState::RESET: case EPaperState::RESET_END: - if (this->reset_()) { - this->set_state_(EPaperState::UPDATE); + if (this->reset()) { + this->set_state_(EPaperState::INITIALISE); } else { - this->set_state_(EPaperState::RESET_END); + this->set_state_(EPaperState::RESET_END, this->reset_duration_); } break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda - this->set_state_(EPaperState::INITIALISE); + if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) { + this->set_state_(EPaperState::IDLE); + return; + } + this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: this->initialise_(); @@ -190,6 +195,10 @@ void EPaperBase::process_state_() { if (!this->transfer_data()) { return; // Not done yet, come back next loop } + this->x_low_ = this->width_; + this->x_high_ = 0; + this->y_low_ = this->height_; + this->y_high_ = 0; this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: @@ -197,7 +206,8 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: - this->refresh_screen(); + this->refresh_screen(this->update_count_ != 0); + this->update_count_ = (this->update_count_ + 1) % this->full_update_every_; this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: @@ -207,6 +217,7 @@ void EPaperBase::process_state_() { case EPaperState::DEEP_SLEEP: this->deep_sleep(); this->set_state_(EPaperState::IDLE); + ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_); break; } } @@ -222,6 +233,9 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) { } ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, TRUEFALSE(this->waiting_for_idle_)); + if (state == EPaperState::IDLE) { + this->disable_loop(); + } } void EPaperBase::start_command_() { @@ -260,20 +274,73 @@ void EPaperBase::initialise_() { 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; } } } +/** + * Check and rotate coordinates based on the transform flags. + * @param x + * @param y + * @return false if the coordinates are out of bounds + */ +bool EPaperBase::rotate_coordinates_(int &x, int &y) const { + if (!this->get_clipping().inside(x, y)) + return false; + if (this->transform_ & SWAP_XY) + std::swap(x, y); + if (this->transform_ & MIRROR_X) + x = this->width_ - x - 1; + if (this->transform_ & MIRROR_Y) + y = this->height_ - y - 1; + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) + return false; + return true; +} + +/** + * Default implementation for monochrome displays where 8 pixels are packed to a byte. + * @param x + * @param y + * @param color + */ +void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { + if (!rotate_coordinates_(x, y)) + return; + const size_t pixel_position = y * this->width_ + x; + const size_t byte_position = pixel_position / 8; + const uint8_t bit_position = pixel_position % 8; + const uint8_t pixel_bit = 0x80 >> bit_position; + const auto original = this->buffer_[byte_position]; + if ((color_to_bit(color) == 0)) { + this->buffer_[byte_position] = original & ~pixel_bit; + } else { + this->buffer_[byte_position] = original | pixel_bit; + } + this->x_low_ = clamp_at_most(this->x_low_, x); + this->x_high_ = clamp_at_least(this->x_high_, x + 1); + this->y_low_ = clamp_at_most(this->y_low_, y); + this->y_high_ = clamp_at_least(this->y_high_, y + 1); +} + 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_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, + " SPI Data Rate: %uMHz\n" + " Full update every: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s", + (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, YESNO(this->transform_ & SWAP_XY), + YESNO(this->transform_ & MIRROR_X), YESNO(this->transform_ & MIRROR_Y)); } } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 4745ec7339..544ea3e9ba 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -5,8 +5,6 @@ #include "esphome/components/split_buffer/split_buffer.h" #include "esphome/core/component.h" -#include - namespace esphome::epaper_spi { using namespace display; @@ -25,10 +23,16 @@ enum class EPaperState : uint8_t { DEEP_SLEEP, // deep sleep the display }; -static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t NONE = 0; +static constexpr uint8_t MIRROR_X = 1; +static constexpr uint8_t MIRROR_Y = 2; +static constexpr uint8_t SWAP_XY = 4; + +static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr size_t MAX_TRANSFER_SIZE = 128; static constexpr uint8_t DELAY_FLAG = 0xFF; -class EPaperBase : public DisplayBuffer, +class EPaperBase : public Display, public spi::SPIDevice { public: @@ -45,6 +49,8 @@ class EPaperBase : public DisplayBuffer, 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 set_transform(uint8_t transform) { this->transform_ = transform; } + void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; } void dump_config() override; void command(uint8_t value); @@ -60,20 +66,47 @@ class EPaperBase : public DisplayBuffer, DisplayType get_display_type() override { return this->display_type_; }; + // Default implementations for monochrome displays + static uint8_t color_to_bit(Color color) { + // It's always a shade of gray. Map to BLACK or WHITE. + // We split the luminance at a suitable point + if ((static_cast(color.r) + color.g + color.b) > 512) { + return 1; + } + return 0; + } + void fill(Color color) override { + auto pixel_color = color_to_bit(color) ? 0xFF : 0x00; + + // We store 8 pixels per byte + this->buffer_.fill(pixel_color); + this->x_high_ = this->width_; + this->y_high_ = this->height_; + this->x_low_ = 0; + this->y_low_ = 0; + } + + void clear() override { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); + } + protected: int get_height_internal() override { return this->height_; }; int get_width_internal() override { return this->width_; }; + int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; } + int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; } + void draw_pixel_at(int x, int y, Color color) override; void process_state_(); const char *epaper_state_to_string_(); bool is_idle_() const; void setup_pins_() const; - bool reset_() const; + virtual bool reset(); 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(); }; + bool rotate_coordinates_(int &x, int &y) const; /** * Methods that must be implemented by concrete classes to control the display @@ -86,7 +119,7 @@ class EPaperBase : public DisplayBuffer, /** * Refresh the screen after data transfer */ - virtual void refresh_screen() = 0; + virtual void refresh_screen(bool partial) = 0; /** * Power the display on @@ -118,24 +151,31 @@ class EPaperBase : public DisplayBuffer, DisplayType display_type_; 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 - + size_t current_data_index_{}; // used by data transfer to track progress + split_buffer::SplitBuffer buffer_{}; GPIOPin *dc_pin_{}; GPIOPin *busy_pin_{}; GPIOPin *reset_pin_{}; + bool waiting_for_idle_{}; + uint32_t delay_until_{}; + uint8_t transform_{}; + uint8_t update_count_{}; + // these values represent the bounds of the updated buffer. Note that x_high and y_high + // point to the pixel past the last one updated, i.e. may range up to width/height. + uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{}; - bool waiting_for_idle_{false}; - uint32_t delay_until_{0}; - - split_buffer::SplitBuffer buffer_; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t waiting_for_idle_last_print_{}; + uint32_t waiting_for_idle_start_{}; +#endif +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + uint32_t update_start_time_{}; +#endif + // properties with specific initialisers go last EPaperState state_{EPaperState::IDLE}; + uint32_t reset_duration_{10}; + uint8_t full_update_every_{1}; }; } // 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 8e4cbdde2a..d0e68595d0 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -6,7 +6,6 @@ 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; enum E6Color { @@ -75,24 +74,24 @@ static uint8_t color_to_hex(Color color) { } void EPaperSpectraE6::power_on() { - ESP_LOGD(TAG, "Power on"); + ESP_LOGV(TAG, "Power on"); this->command(0x04); } void EPaperSpectraE6::power_off() { - ESP_LOGD(TAG, "Power off"); + ESP_LOGV(TAG, "Power off"); this->command(0x02); this->data(0x00); } -void EPaperSpectraE6::refresh_screen() { - ESP_LOGD(TAG, "Refresh"); +void EPaperSpectraE6::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh"); this->command(0x12); this->data(0x00); } void EPaperSpectraE6::deep_sleep() { - ESP_LOGD(TAG, "Deep sleep"); + ESP_LOGV(TAG, "Deep sleep"); this->command(0x07); this->data(0xA5); } @@ -109,12 +108,11 @@ void EPaperSpectraE6::clear() { this->fill(COLOR_ON); } -void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) +void HOT EPaperSpectraE6::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) return; - auto pixel_bits = color_to_hex(color); - uint32_t pixel_position = x + y * this->get_width_controller(); + uint32_t pixel_position = x + y * this->get_width_internal(); uint32_t byte_position = pixel_position / 2; auto original = this->buffer_[byte_position]; if ((pixel_position & 1) != 0) { @@ -128,10 +126,6 @@ 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) { -#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); } @@ -160,7 +154,6 @@ bool HOT EPaperSpectraE6::transfer_data() { this->end_data_(); } this->current_data_index_ = 0; - ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_); return true; } } // 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 48356ad74b..b8dbf0b0c5 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -16,11 +16,11 @@ class EPaperSpectraE6 : public EPaperBase { void clear() override; protected: - void refresh_screen() override; + void refresh_screen(bool partial) 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; + void draw_pixel_at(int x, int y, Color color) override; bool transfer_data() override; }; diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp new file mode 100644 index 0000000000..e4f04657ad --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp @@ -0,0 +1,86 @@ +#include "epaper_spi_ssd1677.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.ssd1677"; + +void EPaperSSD1677::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh screen"); + this->command(0x22); + this->data(partial ? 0xFF : 0xF7); + this->command(0x20); +} + +void EPaperSSD1677::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->command(0x10); +} + +bool EPaperSSD1677::reset() { + if (EPaperBase::reset()) { + this->command(0x12); + return true; + } + return false; +} + +bool HOT EPaperSSD1677::transfer_data() { + auto start_time = millis(); + if (this->current_data_index_ == 0) { + uint8_t data[4]{}; + // round to byte boundaries + this->x_low_ &= ~7; + this->y_low_ &= ~7; + this->x_high_ += 7; + this->x_high_ &= ~7; + this->y_high_ += 7; + this->y_high_ &= ~7; + data[0] = this->x_low_; + data[1] = this->x_low_ / 256; + data[2] = this->x_high_ - 1; + data[3] = (this->x_high_ - 1) / 256; + cmd_data(0x4E, data, 2); + cmd_data(0x44, data, sizeof(data)); + data[0] = this->y_low_; + data[1] = this->y_low_ / 256; + data[2] = this->y_high_ - 1; + data[3] = (this->y_high_ - 1) / 256; + cmd_data(0x4F, data, 2); + this->cmd_data(0x45, data, sizeof(data)); + // for monochrome, we still need to clear the red data buffer at least once to prevent it + // causing dirty pixels after partial refresh. + this->command(this->send_red_ ? 0x26 : 0x24); + this->current_data_index_ = this->y_low_; // actually current line + } + size_t row_length = (this->x_high_ - this->x_low_) / 8; + FixedVector bytes_to_send{}; + bytes_to_send.init(row_length); + ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis()); + this->start_data_(); + while (this->current_data_index_ != this->y_high_) { + size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8; + for (size_t i = 0; i != row_length; i++) { + bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++]; + } + ++this->current_data_index_; + this->write_array(&bytes_to_send.front(), row_length); // NOLINT + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->end_data_(); + return false; + } + } + + this->end_data_(); + this->current_data_index_ = 0; + if (this->send_red_) { + this->send_red_ = false; + return false; + } + return true; +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.h b/esphome/components/epaper_spi/epaper_spi_ssd1677.h new file mode 100644 index 0000000000..47584d24c0 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.h @@ -0,0 +1,25 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +class EPaperSSD1677 : public EPaperBase { + public: + EPaperSSD1677(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_BINARY) { + this->buffer_length_ = width * height / 8; // 8 pixels per byte + } + + protected: + void refresh_screen(bool partial) override; + void power_on() override {} + void power_off() override{}; + void deep_sleep() override; + bool reset() override; + bool transfer_data() override; + bool send_red_{true}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/ssd1677.py b/esphome/components/epaper_spi/models/ssd1677.py new file mode 100644 index 0000000000..3eb53d650e --- /dev/null +++ b/esphome/components/epaper_spi/models/ssd1677.py @@ -0,0 +1,42 @@ +from esphome.const import CONF_DATA_RATE + +from . import EpaperModel + + +class SSD1677(EpaperModel): + def __init__(self, name, class_name="EPaperSSD1677", **kwargs): + if CONF_DATA_RATE not in kwargs: + kwargs[CONF_DATA_RATE] = "20MHz" + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, _height = self.get_dimensions(config) + return ( + (0x18, 0x80), # Select internal Temp sensor + (0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2 + (0x01, (width - 1) % 256, (width - 1) // 256, 0x02), # Set column gate limit + (0x3C, 0x01), # Set border waveform + (0x11, 3), # Set transform + ) + + +ssd1677 = SSD1677("ssd1677") + +ssd1677.extend( + "seeed-ee04-mono-4.26", + width=800, + height=480, + mirror_x=True, + cs_pin=44, + dc_pin=10, + reset_pin=38, + busy_pin={ + "number": 4, + "inverted": False, + "mode": { + "input": True, + "pulldown": True, + }, + }, +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index cff1f51897..d330b4127d 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -19,3 +19,8 @@ display: - platform: epaper_spi model: seeed-reterminal-e1002 + - platform: epaper_spi + model: seeed-ee04-mono-4.26 + # Override pins to avoid conflict with other display configs + busy_pin: 43 + dc_pin: 42